diff --git a/.eslintignore b/.eslintignore index 93de4b10dfee9ebf31933d514ae3cb66e034ad32..b4bfa5a1f7ac085334e906974b5826169c815ee1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ /coverage/ /coverage-javascript/ +/node_modules/ /public/ /tmp/ /vendor/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e522d47d19d00e5a8cae88f47ce5629c2495d233..68690ff33daea76c4d2d61d45975fad76b2d5f29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ variables: USE_BUNDLE_INSTALL: "true" GIT_DEPTH: "20" PHANTOMJS_VERSION: "2.1.1" + GET_SOURCES_ATTEMPTS: "3" before_script: - source ./scripts/prepare_build.sh @@ -232,7 +233,13 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 script: - bundle exec $CI_BUILD_NAME -rubocop: *exec +rubocop: + <<: *ruby-static-analysis + <<: *dedicated-runner + stage: test + script: + - bundle exec "rubocop --require rubocop-rspec" + rake haml_lint: *exec rake scss_lint: *exec rake brakeman: *exec @@ -424,7 +431,7 @@ notify:slack: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" script: - - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>" + - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>" when: on_failure only: - master@gitlab-org/gitlab-ce diff --git a/.haml-lint.yml b/.haml-lint.yml index da9a43d9c6dcf23575c20b89e3bfb569d6530749..7c8a9c4fd1784ac737cd5773944a87b4909496f9 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -7,10 +7,10 @@ exclude: linters: AltText: - enabled: false + enabled: true ClassAttributeWithStaticValue: - enabled: false + enabled: true ClassesBeforeIds: enabled: false @@ -29,14 +29,14 @@ linters: enabled: true FinalNewline: - enabled: false + enabled: true present: true HtmlAttributes: - enabled: false + enabled: true ImplicitDiv: - enabled: false + enabled: true LeadingCommentSpace: enabled: false @@ -80,10 +80,10 @@ linters: enabled: false SpaceBeforeScript: - enabled: false + enabled: true SpaceInsideHashAttributes: - enabled: false + enabled: true style: space Indentation: @@ -94,7 +94,7 @@ linters: enabled: true TrailingWhitespace: - enabled: false + enabled: true UnnecessaryInterpolation: enabled: false diff --git a/.rubocop.yml b/.rubocop.yml index 13df3f996139ac85da048dd76d0736a04494154f..bf2b2d8afc2a2b5c6faac0cfbb4a87ac257524de 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout: # Checks indentation of binary operations that span more than one line. Style/MultilineOperationIndentation: - Enabled: false + Enabled: true + EnforcedStyle: indented # Avoid multi-line `? :` (the ternary operator), use if/unless instead. Style/MultilineTernaryOperator: @@ -342,10 +343,6 @@ Style/ParenthesesAroundCondition: Style/RedundantParentheses: Enabled: true -# Don't use return where it's not required. -Style/RedundantReturn: - Enabled: true - # Don't use semicolons to terminate expressions. Style/Semicolon: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 11b34fafa2ae122534e8ba213e58467c89fab095..d581610162fe58d6b0e237bd94b94397f683d273 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,55 +1,70 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 0` -# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0. +# on 2017-01-11 09:38:25 +0000 using RuboCop version 0.46.0. # 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: 160 +# Offense count: 27 +# Configuration parameters: Include. +# Include: **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Enabled: false + +# Offense count: 175 Lint/AmbiguousRegexpLiteral: Enabled: false -# Offense count: 40 +# Offense count: 53 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Enabled: false -# Offense count: 18 +# Offense count: 1 +Lint/EmptyWhen: + Enabled: false + +# Offense count: 20 Lint/HandleExceptions: Enabled: false -# Offense count: 2 +# Offense count: 1 Lint/Loop: Enabled: false -# Offense count: 19 +# Offense count: 27 Lint/ShadowingOuterLocalVariable: Enabled: false -# Offense count: 9 +# Offense count: 10 # Cop supports --auto-correct. Lint/UnifiedInteger: Enabled: false -# Offense count: 13 +# Offense count: 21 # Cop supports --auto-correct. Lint/UnneededSplatExpansion: Enabled: false -# Offense count: 69 +# Offense count: 82 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Enabled: false -# Offense count: 144 +# Offense count: 173 # Cop supports --auto-correct. # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. Lint/UnusedMethodArgument: Enabled: false -# Offense count: 2 +# Offense count: 93 +# Configuration parameters: CountComments. +Metrics/BlockLength: + Enabled: false + +# Offense count: 3 # Cop supports --auto-correct. Performance/RedundantBlockCall: Enabled: false @@ -59,7 +74,7 @@ Performance/RedundantBlockCall: Performance/RedundantMatch: Enabled: false -# Offense count: 26 +# Offense count: 32 # Cop supports --auto-correct. # Configuration parameters: MaxKeyValuePairs. Performance/RedundantMerge: @@ -69,61 +84,89 @@ Performance/RedundantMerge: RSpec/BeEql: Enabled: false -# Offense count: 20 +# Offense count: 15 # Configuration parameters: CustomIncludeMethods. RSpec/EmptyExampleGroup: Enabled: false -# Offense count: 16 +# Offense count: 24 RSpec/ExpectActual: Enabled: false -# Offense count: 34 +# Offense count: 58 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: implicit, each, example RSpec/HookArgument: Enabled: false -# Offense count: 168 +# Offense count: 12 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: is_expected, should +RSpec/ImplicitExpect: + Enabled: false + +# Offense count: 237 RSpec/LeadingSubject: Enabled: false -# Offense count: 162 +# Offense count: 253 RSpec/LetSetup: Enabled: false -# Offense count: 10 +# Offense count: 13 RSpec/MessageChain: Enabled: false -# Offense count: 714 +# Offense count: 479 # Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: allow, expect -RSpec/MessageExpectation: +# SupportedStyles: have_received, receive +RSpec/MessageSpies: Enabled: false -# Offense count: 2423 +# Offense count: 3036 RSpec/MultipleExpectations: - Max: 36 + Enabled: false -# Offense count: 1504 +# Offense count: 2133 RSpec/NamedSubject: Enabled: false -# Offense count: 1335 +# Offense count: 1974 # Configuration parameters: MaxNesting. RSpec/NestedGroups: Enabled: false -# Offense count: 99 +# Offense count: 32 +RSpec/RepeatedDescription: + Enabled: false + +# Offense count: 1 +RSpec/SingleArgumentMessageChain: + Enabled: false + +# Offense count: 133 RSpec/SubjectStub: Enabled: false -# Offense count: 64 +# Offense count: 104 +# Cop supports --auto-correct. +# Configuration parameters: Whitelist. +# Whitelist: find_by_sql +Rails/DynamicFindBy: + Enabled: false + +# Offense count: 932 +# Cop supports --auto-correct. +# Configuration parameters: Include. +# Include: spec/**/*, test/**/* +Rails/HttpPositionalArguments: + Enabled: false + +# Offense count: 55 Rails/OutputSafety: Enabled: false -# Offense count: 151 +# Offense count: 182 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: strict, flexible Rails/TimeZone: @@ -136,33 +179,34 @@ Rails/TimeZone: Rails/Validation: Enabled: false -# Offense count: 2 +# Offense count: 8 # Cop supports --auto-correct. +# Configuration parameters: AutoCorrect. Security/JSONLoad: Enabled: false -# Offense count: 284 +# Offense count: 346 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation Style/AlignParameters: Enabled: false -# Offense count: 28 +# Offense count: 27 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: always, conditionals Style/AndOr: Enabled: false -# Offense count: 52 +# Offense count: 54 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Enabled: false -# Offense count: 291 +# Offense count: 358 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent @@ -173,66 +217,73 @@ Style/BracesAroundHashParameters: Style/CaseEquality: Enabled: false -# Offense count: 26 +# Offense count: 37 # Cop supports --auto-correct. Style/ColonMethodCall: Enabled: false -# Offense count: 2 +# Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: Keywords. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW Style/CommentAnnotation: Enabled: false -# Offense count: 30 +# Offense count: 29 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Enabled: false -# Offense count: 957 +# Offense count: 1210 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: leading, trailing Style/DotPosition: Enabled: false -# Offense count: 13 +# Offense count: 18 Style/DoubleNegation: Enabled: false -# Offense count: 6 +# Offense count: 7 # Cop supports --auto-correct. Style/EachWithObject: Enabled: false -# Offense count: 26 +# Offense count: 24 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: empty, nil, both Style/EmptyElse: Enabled: false -# Offense count: 3 +# Offense count: 4 # Cop supports --auto-correct. Style/EmptyLiteral: Enabled: false -# Offense count: 140 +# Offense count: 57 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: compact, expanded +Style/EmptyMethod: + Enabled: false + +# Offense count: 147 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Style/ExtraSpacing: Enabled: false -# Offense count: 6 +# Offense count: 8 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: format, sprintf, percent Style/FormatString: Enabled: false -# Offense count: 201 +# Offense count: 238 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false @@ -241,27 +292,27 @@ Style/GuardClause: Style/IfInsideElse: Enabled: false -# Offense count: 174 +# Offense count: 173 # Cop supports --auto-correct. # Configuration parameters: MaxLineLength. Style/IfUnlessModifier: Enabled: false -# Offense count: 53 +# Offense count: 55 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets Style/IndentArray: Enabled: false -# Offense count: 95 +# Offense count: 101 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Style/IndentHash: Enabled: false -# Offense count: 29 +# Offense count: 41 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: line_count_dependent, lambda, literal @@ -273,16 +324,21 @@ Style/Lambda: Style/LineEndConcatenation: Enabled: false -# Offense count: 15 +# Offense count: 19 # Cop supports --auto-correct. Style/MethodCallParentheses: Enabled: false -# Offense count: 8 +# Offense count: 9 Style/MethodMissing: Enabled: false -# Offense count: 95 +# Offense count: 3 +# Cop supports --auto-correct. +Style/MultilineIfModifier: + Enabled: false + +# Offense count: 179 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false @@ -299,32 +355,32 @@ Style/NestedParenthesizedCalls: Style/Next: Enabled: false -# Offense count: 12 +# Offense count: 19 # Cop supports --auto-correct. # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. # SupportedOctalStyles: zero_with_o, zero_only Style/NumericLiteralPrefix: Enabled: false -# Offense count: 53 +# Offense count: 19 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false -# Offense count: 29 +# Offense count: 34 # Cop supports --auto-correct. Style/ParallelAssignment: Enabled: false -# Offense count: 294 +# Offense count: 417 # Cop supports --auto-correct. # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: Enabled: false -# Offense count: 11 +# Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: lower_case_q, upper_case_q @@ -336,7 +392,7 @@ Style/PercentQLiterals: Style/PerlBackrefs: Enabled: false -# Offense count: 38 +# Offense count: 64 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ @@ -344,17 +400,19 @@ Style/PerlBackrefs: Style/PredicateName: Enabled: false -# Offense count: 26 +# Offense count: 33 # Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: short, verbose Style/PreferredHashMethods: Enabled: false -# Offense count: 6 +# Offense count: 8 # Cop supports --auto-correct. Style/Proc: Enabled: false -# Offense count: 22 +# Offense count: 50 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: compact, exploded @@ -371,33 +429,34 @@ Style/RedundantBegin: Style/RedundantException: Enabled: false -# Offense count: 24 +# Offense count: 29 # Cop supports --auto-correct. Style/RedundantFreeze: Enabled: false -# Offense count: 427 +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Enabled: false + +# Offense count: 359 # Cop supports --auto-correct. Style/RedundantSelf: Enabled: false -# Offense count: 97 +# Offense count: 105 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Enabled: false -# Offense count: 18 +# Offense count: 19 # Cop supports --auto-correct. Style/RescueModifier: Enabled: false -# Offense count: 114 -# Cop supports --auto-correct. -Style/SafeNavigation: - Enabled: false - # Offense count: 7 # Cop supports --auto-correct. Style/SelfAssignment: @@ -405,7 +464,7 @@ Style/SelfAssignment: # Offense count: 2 # Configuration parameters: Methods. -# Methods: {"reduce"=>["a", "e"]}, {"inject"=>["a", "e"]} +# Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]} Style/SingleLineBlockParams: Enabled: false @@ -415,56 +474,63 @@ Style/SingleLineBlockParams: Style/SingleLineMethods: Enabled: false -# Offense count: 125 +# Offense count: 138 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: space, no_space Style/SpaceBeforeBlockBraces: Enabled: false -# Offense count: 10 +# Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Style/SpaceBeforeFirstArg: Enabled: false -# Offense count: 145 +# Offense count: 37 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: require_no_space, require_space +Style/SpaceInLambdaLiteral: + Enabled: false + +# Offense count: 174 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space Style/SpaceInsideBlockBraces: Enabled: false -# Offense count: 99 +# Offense count: 115 # Cop supports --auto-correct. Style/SpaceInsideBrackets: Enabled: false -# Offense count: 65 +# Offense count: 77 # Cop supports --auto-correct. Style/SpaceInsideParens: Enabled: false -# Offense count: 7 +# Offense count: 4 # Cop supports --auto-correct. Style/SpaceInsidePercentLiteralDelimiters: Enabled: false -# Offense count: 41 +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: SupportedStyles. # SupportedStyles: use_perl_names, use_english_names Style/SpecialGlobalVars: EnforcedStyle: use_perl_names -# Offense count: 31 +# Offense count: 25 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: single_quotes, double_quotes Style/StringLiteralsInInterpolation: Enabled: false -# Offense count: 33 +# Offense count: 54 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. # IgnoredMethods: respond_to, define_method @@ -474,18 +540,18 @@ Style/SymbolProc: # Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex Style/TernaryParentheses: Enabled: false -# Offense count: 29 +# Offense count: 36 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma Style/TrailingCommaInArguments: Enabled: false -# Offense count: 102 +# Offense count: 150 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma @@ -498,12 +564,12 @@ Style/TrailingCommaInLiteral: Style/TrailingUnderscoreVariable: Enabled: false -# Offense count: 76 +# Offense count: 67 # Cop supports --auto-correct. Style/TrailingWhitespace: Enabled: false -# Offense count: 2 +# Offense count: 3 # 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 @@ -515,7 +581,7 @@ Style/TrivialAccessors: Style/UnlessElse: Enabled: false -# Offense count: 14 +# Offense count: 17 # Cop supports --auto-correct. Style/UnneededInterpolation: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index fb13db4dd1c95892c4a9514013ff1c72e97b9745..cabfef84b24da9fb378d2c02f03bce384d6f470e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,289 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.15.4 (2017-01-09) + +- Make successful pipeline emails off for watchers. !8176 +- Speed up group milestone index by passing group_id to IssuesFinder. !8363 +- Don't instrument 405 Grape calls. !8445 +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. +- Re-order update steps in the 8.14 -> 8.15 upgrade guide. +- Re-add Google Cloud Storage as a backup strategy. + +## 8.15.3 (2017-01-06) + +- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !8425 +- Rename projects wth reserved names. !8234 +- Cache project authorizations even when user has access to zero projects. !8327 +- Fix a minor grammar error in merge request widget. !8337 +- Fix unclear closing issue behaviour on Merge Request show page. !8345 (Gabriel Gizotti) +- fix border in login session tabs. !8346 +- Copy, don't move uploaded avatar files. !8396 +- Increases width of mini-pipeline-graph dropdown to prevent wrong position on chrome on ubuntu. !8399 +- Removes invalid html and unneed CSS to prevent shaking in the pipelines tab. !8411 +- Gitlab::LDAP::Person uses LDAP attributes configuration. !8418 +- Fix 500 errors when creating a user with identity via API. !8442 +- Whitelist next project names: assets, profile, public. !8470 +- Fixed regression of note-headline-light where it was always placed on 2 lines, even on wide viewports. +- Fix 500 error when visit group from admin area if group name contains dot. +- Fix cross-project references copy to include the project reference. +- Fix 500 error renaming group. +- Fixed GFM dropdown not showing on new lines. + +## 8.15.2 (2016-12-27) + +- Fix finding the latest pipeline. !8301 +- Fix mr list timestamp alignment. !8271 +- Fix discussion overlap text in regular screens. !8273 +- Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari. !8282 +- Fix line breaking in nodes of the pipeline graph in firefox. !8292 +- Fixes confendential warning text alignment. !8293 +- Hide Scroll Top button for failed build page. !8295 +- Fix finding the latest pipeline. !8301 +- Disable PostgreSQL statement timeouts when removing unneeded services. !8322 +- Fix timeout when MR contains large files marked as binary by .gitattributes. +- Rename "autodeploy" to "auto deploy". +- Fixed GFM autocomplete error when no data exists. +- Fixed resolve discussion note button color. + +## 8.15.1 (2016-12-23) + +- Push payloads schedule at most 100 commits, instead of all commits. +- Fix Mattermost command creation by specifying username. +- Do not override incoming webhook for mattermost and slack. +- Adds background color for disabled state to merge when succeeds dropdown. !8222 +- Standardises font-size for titles in Issues, Merge Requests and Merge Request widget. !8235 +- Fix Pipeline builds list blank on MR. !8255 +- Do not show retried builds in pipeline stage dropdown. !8260 + +## 8.15.0 (2016-12-22) + +- Whitelist next project names: notes, services. +- Use Grape's new Route methods. +- Fixed issue boards scrolling with a lot of lists & issues. +- Remove unnecessary sentences for status codes in the API documentation. (Luis Alonso Chavez Armendariz) +- Allow unauthenticated access to Repositories Files API GET endpoints. +- Add note to the invite page when the logged in user email is not the same as the invitation. +- Don't accidentally mark unsafe diff lines as HTML safe. +- Add git diff context to notifications of new notes on merge requests. (Heidi Hoopes) +- Shows group members in project members list. +- Gem update: Update grape to 0.18.0. (Robert Schilling) +- API: Expose merge status for branch API. (Robert Schilling) +- Displays milestone remaining days only when it's present. +- API: Expose committer details for commits. (Robert Schilling) +- API: Ability to set 'should_remove_source_branch' on merge requests. (Robert Schilling) +- Fix project import label priorities error. +- Fix Import/Export merge requests error while importing. +- Refactor Bitbucket importer to use BitBucket API Version 2. +- Fix Import/Export duplicated builds error. +- Ci::Builds have same ref as Ci::Pipeline in dev fixtures. (twonegatives) +- For single line git commit messages, the close quote should be on the same line as the open quote. +- Use authorized projects in ProjectTeam. +- Destroy a user's session when they delete their own account. +- Edit help text to clarify annotated tag creation. (Liz Lam) +- Fixed file template dropdown for the "New File" editor for smaller/zoomed screens. +- Fix Route#rename_children behavior. +- Add nested groups support on data level. +- Allow projects with 'dashboard' as path. +- Disabled emoji buttons when user is not logged in. +- Remove unused and void services from the database. +- Add issue search slash command. +- Accept issue new as command to create an issue. +- Non members cannot create labels through the API. +- API: expose pipeline coverage. +- Validate state param when filtering issuables. +- Username exists check respects relative root path. +- Bump Git version requirement to 2.8.4. +- Updates the font weight of button styles because of the change to system fonts. +- Update API spec files to describe the correct class. (Livier) +- Fixed timeago re-rendering every timeago. +- Enable ColorVariable in scss-lint. (Sam Rose) +- Various small emoji positioning adjustments. +- Add shortcuts for adding users to a project team with a specific role. (Nikolay Ponomarev and Dino M) +- Additional rounded label fixes. +- Remove unnecessary database indices. +- 24726 Remove Across GitLab from side navigation. +- Changed cursor icon to pointer when mousing over stages on the Cycle Analytics pages. (Ryan Harris) +- Add focus state to dropdown items. +- Fixes Environments displaying incorrect date since 8.14 upgrade. +- Improve bulk assignment for issuables. +- Stop supporting Google and Azure as backup strategies. +- Fix broken README.md UX guide link. +- Allow public access to some Tag API endpoints. +- Encode input when migrating ProcessCommitWorker jobs to prevent migration errors. +- Adjust the width of project avatars to fix alignment within their container. (Ryan Harris) +- Sentence cased the nav tab headers on the project dashboard page. (Ryan Harris) +- Adds hoverstates for collapsed Issue/Merge Request sidebar. +- Make CI badge hitboxes match parent. +- Add a starting date to milestones. +- Adjusted margins for Build Status and Coverage Report rows to match those of the CI/CD Pipeline row. (Ryan Harris) +- Updated members dropdowns. +- Move all action buttons to project header. +- Replace issue access checks with use of IssuableFinder. +- Fix missing Note access checks by moving Note#search to updated NoteFinder. +- Centered Accept Merge Request button within MR widget and added padding for viewports smaller than 768px. (Ryan Harris) +- Fix missing access checks on issue lookup using IssuableFinder. +- Added top margin to Build status page header for mobile views. (Ryan Harris) +- Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" on MR pages. +- Issue#visible_to_user moved to IssuesFinder to prevent accidental use. +- Replace MR access checks with use of MergeRequestsFinder. +- Fix information disclosure in `Projects::BlobController#update`. +- Allow branch names with dots on API endpoint. +- Changed Housekeeping button on project settings page to default styling. (Ryan Harris) +- Ensure issuable state changes only fire webhooks once. +- Fix bad selection on dropdown menu for tags filter. (Luis Alonso Chavez Armendariz) +- Fix title case to sentence case. (Luis Alonso Chavez Armendariz) +- Fix appearance in error pages. (Luis Alonso Chavez Armendariz) +- Create mattermost service. +- 25617 Fix placeholder color of todo filters. +- Made the padding on the plus button in the breadcrumb menu even. (Ryan Harris) +- Allow to delete tag release note. +- Ensure nil User-Agent doesn't break the CI API. +- Replace Rack::Multipart with GitLab-Workhorse based solution. !5867 +- Add scopes for personal access tokens and OAuth tokens. !5951 +- API: Endpoint to expose personal snippets as /snippets. !6373 (Bernard Guyzmo Pratz) +- New `gitlab:workhorse:install` rake task. !6574 +- Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742. !6635 (Makoto Scott-Hinkle) +- Add support for setting the GitLab Runners Registration Token during initial database seeding. !6642 +- Guests can read builds when public. !6842 +- Made comment autocomplete more performant and removed some loading bugs. !6856 +- Add GitLab host to 2FA QR code and manual info. !6941 +- Add sorting functionality for group/project members. !7032 +- Rename Merge When Build Succeeds to Merge When Pipeline Succeeds. !7135 +- Resolve all discussions in a merge request by creating an issue collecting them. !7180 (Bob Van Landuyt) +- Add Human Readable format for rake backup. !7188 (David GerÅ‘) +- post_receive: accept any user email from last commit. !7225 (Elan Ruusamäe) +- Add support for Dockerfile templates. !7247 +- Add shorthand support to gitlab markdown references. !7255 (Oswaldo Ferreira) +- Display error code for U2F errors. !7305 (winniehell) +- Fix wrong tab selected when loggin fails and multiple login tabs exists. !7314 (Jacopo Beschi @jacopo-beschi) +- Clean up common_utils.js. !7318 (winniehell) +- Show commit status from latest pipeline. !7333 +- Remove the help text under the sidebar subscribe button and style it inline. !7389 +- Update wiki page design. !7429 +- Add nested groups support to the routing. !7459 +- Changed eslint airbnb config to the base airbnb config and corrected eslintrc plugins and envs. !7470 (Luke "Jared" Bennett) +- Fix cancelling created or external pipelines. !7508 +- Allow admins to stop impersonating users without e-mail addresses. !7550 (Oren Kanner) +- Remove unnecessary self from user model. !7551 (Semyon Pupkov) +- Homogenize filter and sort dropdown look'n'feel. !7583 (David Wagner) +- Create dynamic fixture for build_spec. !7589 (winniehell) +- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600 +- Remove unnecessary require_relative calls from service classes. !7601 (Semyon Pupkov) +- Simplify copy on "Create a new list" dropdown in Issue Boards. !7605 (Victor Rodrigues) +- Refactor create service spec. !7609 (Semyon Pupkov) +- Shows unconfirmed email status in profile. !7611 +- The admin user projects view now has a clickable group link. !7620 (James Gregory) +- Prevent DOM ID collisions resulting from user-generated content anchors. !7631 +- Replace static fixture for abuse_reports_spec. !7644 (winniehell) +- Define common helper for describe pagination params in api. !7646 (Semyon Pupkov) +- Move abuse report spinach test to rspec. !7659 (Semyon Pupkov) +- Replace static fixture for awards_handler_spec. !7661 (winniehell) +- API: Add ability to unshare a project from a group. !7662 (Robert Schilling) +- Replace references to MergeRequestDiff#commits with st_commits when we care only about the number of commits. !7668 +- Add issue events filter and make all really show all events. !7673 (Oxan van Leeuwen) +- Replace static fixture for notes_spec. !7683 (winniehell) +- Replace static fixture for shortcuts_issuable_spec. !7685 (winniehell) +- Replace static fixture for zen_mode_spec. !7686 (winniehell) +- Replace static fixture for right_sidebar_spec. !7687 (winniehell) +- Add online terminal support for Kubernetes. !7690 +- Move admin abuse report spinach test to rspec. !7691 (Semyon Pupkov) +- Move admin spam spinach test to Rspec. !7708 (Semyon Pupkov) +- Make API::Helpers find a project with only one query. !7714 +- Create builds in transaction to avoid empty pipelines. !7742 +- Render SVG images in diffs and notes. !7747 (andrebsguedes) +- Add setting to enable/disable HTML emails. !7749 +- Use SmartInterval for MR widget and improve visibilitychange functionality. !7762 +- Resolve "Remove Builds tab from Merge Requests and Commits". !7763 +- Moved new projects button below new group button on the welcome screen. !7770 +- fix display hook error message. !7775 (basyura) +- Refactor issuable_filters_present to reduce duplications. !7776 (Semyon Pupkov) +- Redirect to sign-in page when unauthenticated user tries to create a snippet. !7786 +- Fix Archived project merge requests add to group's Merge Requests. !7790 (Jacopo Beschi @jacopo-beschi) +- Update generic/external build status to match normal build status template. !7811 +- Enable AsciiDoctor admonition icons. !7812 (Horacio Sanson) +- Do not raise error in AutocompleteController#users when not authorized. !7817 (Semyon Pupkov) +- fix: 24982- Remove'Signed in successfully' message After this change the sign-in-success flash message will not be shown. !7837 (jnoortheen) +- Fix Latest deployment link is broken. !7839 +- Don't display prompt to add SSH keys if SSH protocol is disabled. !7840 (Andrew Smith (EspadaV8)) +- Allow unauthenticated access to some Project API GET endpoints. !7843 +- Refactor presenters ChatCommands. !7846 +- Improve help message for issue create slash command. !7850 +- change text around timestamps to make it clear which timestamp is displayed. !7860 (BM5k) +- Improve Build Log scrolling experience. !7895 +- Change ref property to commitRef in vue commit component. !7901 +- Prevent user creating issue or MR without signing in for a group. !7902 +- Provides a sensible default message when adding a README to a project. !7903 +- Bump ruby version to 2.3.3. !7904 +- Fix comments activity tab visibility condition. !7913 (Rydkin Maxim) +- Remove unnecessary target branch link from MR page in case of deleted target branch. !7916 (Rydkin Maxim) +- Add image controls to MR diffs. !7919 +- Remove wrong '.builds-feature' class from the MR settings fieldset. !7930 +- Resolve "Manual actions on pipeline graph". !7931 +- Avoid escaping relative links in Markdown twice. !7940 (winniehell) +- Move admin hooks spinach to rspec. !7942 (Semyon Pupkov) +- Move admin logs spinach test to rspec. !7945 (Semyon Pupkov) +- fix: removed signed_out notification. !7958 (jnoortheen) +- Accept environment variables from the `pre-receive` script. !7967 +- Do not reload diff for merge request made from fork when target branch in fork is updated. !7973 +- Fixes left align issue for long system notes. !7982 +- Add a slug to environments. !7983 +- Fix lookup of project by unknown ref when caching is enabled. !7988 +- Resolve "Provide SVG as a prop instead of hiding and copy them in environments table". !7992 +- Introduce deployment services, starting with a KubernetesService. !7994 +- Adds tests for custom event polyfill. !7996 +- Allow all alphanumeric characters in file names. !8002 (winniehell) +- Added support for math rendering, using KaTeX, in Markdown and asciidoc. !8003 (Munken) +- Remove unnecessary commits order message. !8004 +- API: Memoize the current_user so that sudo can work properly. !8017 +- group authors in contribution graph with case insensitive email handle comparison. !8021 +- Move admin active tab spinach tests to rspec. !8037 (Semyon Pupkov) +- Add Authentiq as Oauth provider. !8038 (Alexandros Keramidas) +- API: Ability to cherry pick a commit. !8047 (Robert Schilling) +- Fix Slack pipeline message from pipelines made by API. !8059 +- API: Simple representation of group's projects. !8060 (Robert Schilling) +- Prevent overflow with vertical scroll when we have space to show content. !8061 +- Allow to auto-configure Mattermost. !8070 +- Introduce $CI_BUILD_REF_SLUG. !8072 +- Added go back anchor on error pages. !8087 +- Convert CI YAML variables keys into strings. !8088 +- Adds Direct link from pipeline list to builds. !8097 +- Cache last commit id for path. !8098 (Hiroyuki Sato) +- Pass variables from deployment project services to CI runner. !8107 +- New Gitea importer. !8116 +- Introduce "Set up autodeploy" button to help configure GitLab CI for deployment. !8135 +- Prevent enviroment table to overflow when name has underscores. !8142 +- Fix missing service error importing from EE to CE. !8144 +- Milestoneish SQL performance partially improved and memoized. !8146 +- Allow unauthenticated access to Repositories API GET endpoints. !8148 +- fix colors and margins for adjacent alert banners. !8151 +- Hides new issue button for non loggedin user. !8175 +- Fix N+1 queries on milestone show pages. !8185 +- Rename groups with .git in the end of the path. !8199 +- Whitelist next project names: help, ci, admin, search. !8227 +- Adds back CSS for progress-bars. !8237 + +## 8.14.6 (2017-01-10) + +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. + +## 8.14.5 (2016-12-14) + +- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600 +- fix display hook error message. !7775 (basyura) +- Remove wrong '.builds-feature' class from the MR settings fieldset. !7930 +- Avoid escaping relative links in Markdown twice. !7940 (winniehell) +- API: Memoize the current_user so that sudo can work properly. !8017 +- Displays milestone remaining days only when it's present. +- Allow branch names with dots on API endpoint. +- Issue#visible_to_user moved to IssuesFinder to prevent accidental use. +- Shows group members in project members list. +- Encode input when migrating ProcessCommitWorker jobs to prevent migration errors. +- Fixed timeago re-rendering every timeago. +- Fix missing Note access checks by moving Note#search to updated NoteFinder. + ## 8.14.4 (2016-12-08) - Fix diff view permalink highlighting. !7090 @@ -264,6 +547,18 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.11 (2017-01-10) + +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. + +## 8.13.10 (2016-12-14) + +- API: Memoize the current_user so that sudo can work properly. !8017 +- Filter `authentication_token`, `incoming_email_token` and `runners_token` parameters. +- Issue#visible_to_user moved to IssuesFinder to prevent accidental use. +- Fix missing Note access checks by moving Note#search to updated NoteFinder. + ## 8.13.9 (2016-12-08) - Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 659871a06a450110cff553ae2a1448955711ecc1..b68c4a67826209c015d4a526f604120ac55f53aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -217,8 +217,8 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, and/or documentation. The features we would really like a merge request for are listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce] and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note -that if an issue is marked for the current milestone either before or while you -are working on it, a team member may take over the merge request in order to +that if an issue is marked for the current milestone either before or while you +are working on it, a team member may take over the merge request in order to ensure the work is finished before the release date. If you want to add a new feature that is not labeled it is best to first create @@ -300,6 +300,7 @@ you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MRs that leads to higher code quality is more important to us than having a minimal commit log. The smaller an MR is the more likely it is it will be merged (quickly). After that you can send more MRs to enhance it. +The ['How to get faster PR reviews' document of Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) also has some great points regarding this. For examples of feedback on merge requests please look at already [closed merge requests][closed-merge-requests]. If you would like quick feedback diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index c4e41f94594c72f3175d1c465d1f360697ba1386..627a3f43a64f9d878a1143f59bea46e1970d0320 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -4.0.3 +4.1.1 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 524cb55242b53f6a64cc646ea05db6acc7696d2d..6085e946503a10fb4d58a5c7d9a6e572c21ddd2e 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.1.1 +1.2.1 diff --git a/Gemfile b/Gemfile index 2cc7764e6b8f048d3338790e3611e3bb14994ae6..9bf37891f7422eee9f12b1715be4a4464b006176 100644 --- a/Gemfile +++ b/Gemfile @@ -16,13 +16,14 @@ gem 'default_value_for', '~> 3.0.0' gem 'mysql2', '~> 0.3.16', group: :mysql gem 'pg', '~> 0.18.2', group: :postgres +gem 'rugged', '~> 0.24.0' + # Authentication libraries gem 'devise', '~> 4.2' gem 'doorkeeper', '~> 4.2.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' -gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-github', '~> 1.1.1' @@ -33,6 +34,7 @@ gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' +gem 'omniauth-authentiq', '~> 0.2.0' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt' @@ -49,10 +51,6 @@ gem 'u2f', '~> 0.2.1' # Browser detection gem 'browser', '~> 2.2' -# Extracting information from a git repository -# Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.7.0' - # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master @@ -67,7 +65,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.15.0' +gem 'grape', '~> 0.18.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' @@ -86,10 +84,14 @@ gem 'dropzonejs-rails', '~> 0.7.1' # for backups gem 'fog-aws', '~> 0.9' gem 'fog-core', '~> 1.40' +gem 'fog-google', '~> 0.5' gem 'fog-local', '~> 0.3' gem 'fog-openstack', '~> 0.1' gem 'fog-rackspace', '~> 0.1.1' +# for Google storage +gem 'google-api-client', '~> 0.8.6' + # for aws storage gem 'unf', '~> 0.1.4' @@ -97,18 +99,19 @@ gem 'unf', '~> 0.1.4' gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing -gem 'html-pipeline', '~> 1.11.0' -gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' -gem 'gitlab-markup', '~> 1.5.0' -gem 'redcarpet', '~> 3.3.3' -gem 'RedCloth', '~> 4.3.2' -gem 'rdoc', '~> 4.2' -gem 'org-ruby', '~> 0.9.12' -gem 'creole', '~> 0.5.0' -gem 'wikicloth', '0.8.1' -gem 'asciidoctor', '~> 1.5.2' -gem 'rouge', '~> 2.0' -gem 'truncato', '~> 0.7.8' +gem 'html-pipeline', '~> 1.11.0' +gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' +gem 'gitlab-markup', '~> 1.5.1' +gem 'redcarpet', '~> 3.3.3' +gem 'RedCloth', '~> 4.3.2' +gem 'rdoc', '~> 4.2' +gem 'org-ruby', '~> 0.9.12' +gem 'creole', '~> 0.5.0' +gem 'wikicloth', '0.8.1' +gem 'asciidoctor', '~> 1.5.2' +gem 'asciidoctor-plantuml', '0.0.6' +gem 'rouge', '~> 2.0' +gem 'truncato', '~> 0.7.8' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM @@ -170,7 +173,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1' gem 'gemnasium-gitlab-service', '~> 0.2' # Slack integration -gem 'slack-notifier', '~> 1.2.0' +gem 'slack-notifier', '~> 1.5.1' # Asana integration gem 'asana', '~> 0.4.0' @@ -178,6 +181,9 @@ gem 'asana', '~> 0.4.0' # FogBugz integration gem 'ruby-fogbugz', '~> 0.2.1' +# Kubernetes integration +gem 'kubeclient', '~> 2.2.0' + # d3 gem 'd3_rails', '~> 3.5.0' @@ -213,11 +219,10 @@ gem 'oj', '~> 2.17.4' gem 'chronic', '~> 0.10.2' gem 'chronic_duration', '~> 0.10.6' -gem 'sass-rails', '~> 5.0.6' +gem 'sassc-rails', '~> 1.3.0' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' -gem 'turbolinks', '~> 2.5.0' -gem 'jquery-turbolinks', '~> 2.1.0' +gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' @@ -250,7 +255,6 @@ group :development do gem 'brakeman', '~> 3.3.0', require: false gem 'letter_opener_web', '~> 1.3.0' - gem 'rerun', '~> 0.11.0' gem 'bullet', '~> 5.2.0', require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'web-console', '~> 2.0' @@ -281,7 +285,7 @@ group :development, :test do gem 'minitest', '~> 5.7.0' # Generate Fake data - gem 'ffaker', '~> 2.0.0' + gem 'ffaker', '~> 2.4' gem 'capybara', '~> 2.6.2' gem 'capybara-screenshot', '~> 1.0.0' @@ -295,8 +299,8 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.43.0', require: false - gem 'rubocop-rspec', '~> 1.5.0', require: false + gem 'rubocop', '~> 0.46.0', require: false + gem 'rubocop-rspec', '~> 1.9.1', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'haml_lint', '~> 0.18.2', require: false gem 'simplecov', '0.12.0', require: false @@ -325,11 +329,11 @@ end gem 'newrelic_rpm', '~> 3.16' -gem 'octokit', '~> 4.3.0' +gem 'octokit', '~> 4.6.2' gem 'mail_room', '~> 0.9.0' -gem 'email_reply_parser', '~> 0.5.8' +gem 'email_reply_trimmer', '~> 0.1' gem 'html2text' gem 'ruby-prof', '~> 0.16.2' @@ -344,5 +348,5 @@ gem 'paranoia', '~> 2.2' gem 'health_check', '~> 2.2.0' # System information -gem 'vmstat', '~> 2.2' +gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' diff --git a/Gemfile.lock b/Gemfile.lock index 3de1a7cbf262178febf56fcfbfaee170cd7cd9a3..4d025683b205404bb2cacf0002476a56bf3d42f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,10 +54,16 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.3) + asciidoctor-plantuml (0.0.6) + asciidoctor (~> 1.5) ast (2.3.0) attr_encrypted (3.0.3) encryptor (~> 3.0.0) attr_required (1.0.0) + autoparse (0.3.3) + addressable (>= 2.3.1) + extlib (>= 0.9.15) + multi_json (>= 1.0.0) autoprefixer-rails (6.2.3) execjs json @@ -161,11 +167,13 @@ GEM diff-lcs (1.2.5) diffy (3.1.0) docile (1.1.5) + domain_name (0.5.20161021) + unf (>= 0.0.5, < 1.0.0) doorkeeper (4.2.0) railties (>= 4.2) dropzonejs-rails (0.7.2) rails (> 3.1) - email_reply_parser (0.5.8) + email_reply_trimmer (0.1.6) email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) @@ -177,6 +185,7 @@ GEM excon (0.52.0) execjs (2.6.0) expression_parser (0.9.0) + extlib (0.9.16) factory_girl (4.7.0) activesupport (>= 3.0.0) factory_girl_rails (4.7.0) @@ -189,7 +198,7 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json - ffaker (2.0.0) + ffaker (2.4.0) ffi (1.9.10) flay (2.6.1) ruby_parser (~> 3.0) @@ -206,6 +215,10 @@ GEM builder excon (~> 0.49) formatador (~> 0.2) + fog-google (0.5.0) + fog-core + fog-json + fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) @@ -252,12 +265,9 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab-markup (1.5.0) - gitlab_git (10.7.0) - activesupport (~> 4.0) - charlock_holmes (~> 0.7.3) - github-linguist (~> 4.7.0) - rugged (~> 0.24.0) + gitlab-markup (1.5.1) + gitlab-turbolinks-classic (2.5.6) + coffee-rails gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) @@ -282,15 +292,34 @@ GEM json multi_json request_store (>= 1.0) - grape (0.15.0) + google-api-client (0.8.7) + activesupport (>= 3.2, < 5.0) + addressable (~> 2.3) + autoparse (~> 0.3) + extlib (~> 0.9) + faraday (~> 0.9) + googleauth (~> 0.3) + launchy (~> 2.4) + multi_json (~> 1.10) + retriable (~> 1.4) + signet (~> 0.6) + googleauth (0.5.1) + faraday (~> 0.9) + jwt (~> 1.4) + logging (~> 2.0) + memoist (~> 0.12) + multi_json (~> 1.11) + os (~> 0.9) + signet (~> 0.7) + grape (0.18.0) activesupport builder hashie (>= 2.1.0) multi_json (>= 1.3.2) multi_xml (>= 0.5.2) + mustermann-grape (~> 0.4.0) rack (>= 1.3.0) rack-accept - rack-mount virtus (>= 1.0.0) grape-entity (0.6.0) activesupport @@ -318,6 +347,15 @@ GEM html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) + http (0.9.8) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 1.0.1) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (1.0.1) + http_parser.rb (0.6.0) httparty (0.13.7) json (~> 1.8) multi_xml (>= 0.5.2) @@ -336,9 +374,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-turbolinks (2.1.0) - railties (>= 3.1.0) - turbolinks jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) @@ -352,6 +387,10 @@ GEM knapsack (1.11.0) rake timecop (>= 0.1.0) + kubeclient (2.2.0) + http (= 0.9.8) + recursive-open-struct (= 1.0.0) + rest-client launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) @@ -368,14 +407,16 @@ GEM xml-simple licensee (8.0.0) rugged (>= 0.24b) - listen (3.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) + little-plugger (1.1.4) + logging (2.1.0) + little-plugger (~> 1.1) + multi_json (~> 1.10) loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) mail_room (0.9.0) + memoist (0.15.0) method_source (0.8.2) mime-types (2.99.3) mimemagic (0.3.0) @@ -385,9 +426,14 @@ GEM multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) + mustermann (0.4.0) + tool (~> 0.2) + mustermann-grape (0.4.0) + mustermann (= 0.4.0) mysql2 (0.3.20) net-ldap (0.12.1) net-ssh (3.0.1) + netrc (0.11.0) newrelic_rpm (3.16.0.318) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) @@ -400,22 +446,20 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - octokit (4.3.0) - sawyer (~> 0.7.0, >= 0.5.3) + octokit (4.6.2) + sawyer (~> 0.8.0, >= 0.5.3) oj (2.17.4) omniauth (1.3.1) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) omniauth-oauth2 (~> 1.1) + omniauth-authentiq (0.2.2) + omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-bitbucket (0.0.2) - multi_json (~> 1.7) - omniauth (~> 1.1) - omniauth-oauth (~> 1.0) omniauth-cas3 (1.1.3) addressable (~> 2.3) nokogiri (~> 1.6.6) @@ -460,6 +504,7 @@ GEM org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) + os (0.9.6) paranoia (2.2.0) activerecord (>= 4.0, < 5.1) parser (2.3.1.4) @@ -489,14 +534,12 @@ GEM pry-rails (0.3.4) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - rack (1.6.4) + rack (1.6.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) rack rack-cors (0.4.0) - rack-mount (0.8.3) - rack (>= 1.0.0) rack-oauth2 (1.2.3) activesupport (>= 2.3) attr_required (>= 0.0.5) @@ -534,15 +577,13 @@ GEM rainbow (2.1.0) raindrops (0.17.0) rake (10.5.0) - rb-fsevent (0.9.6) - rb-inotify (0.9.5) - ffi (>= 0.5.0) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) rdoc (4.2.2) json (~> 1.4) recaptcha (3.0.0) json + recursive-open-struct (1.0.0) redcarpet (3.3.3) redis (3.2.2) redis-actionpack (5.0.1) @@ -564,10 +605,13 @@ GEM redis-store (1.2.0) redis (>= 2.2) request_store (1.3.1) - rerun (0.11.0) - listen (~> 3.0) responders (2.3.0) railties (>= 4.2.0, < 5.1) + rest-client (2.0.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + retriable (1.4.1) rinku (2.0.0) rotp (2.1.2) rouge (2.0.7) @@ -598,14 +642,14 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.5.0) - rubocop (0.43.0) + rubocop (0.46.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.5.0) - rubocop (>= 0.40.0) + rubocop-rspec (1.9.1) + rubocop (>= 0.42.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-prof (0.16.2) @@ -623,15 +667,20 @@ GEM sanitize (2.1.0) nokogiri (>= 1.4.4) sass (3.4.22) - sass-rails (5.0.6) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - sawyer (0.7.0) - addressable (>= 2.3.5, < 2.5) - faraday (~> 0.8, < 0.10) + sassc (1.11.1) + bundler + ffi (~> 1.9.6) + sass (>= 3.3.0) + sassc-rails (1.3.0) + railties (>= 4.0.0) + sass + sassc (~> 1.9) + sprockets (> 2.11) + sprockets-rails + tilt + sawyer (0.8.1) + addressable (>= 2.3.5, < 2.6) + faraday (~> 0.8, < 1.0) scss_lint (0.47.1) rake (>= 0.9, < 11) sass (~> 3.4.15) @@ -659,12 +708,17 @@ GEM sidekiq (>= 4.2.1) sidekiq-limit_fetch (3.4.0) sidekiq (>= 4) + signet (0.7.3) + addressable (~> 2.3) + faraday (~> 0.9) + jwt (~> 1.5) + multi_json (~> 1.10) simplecov (0.12.0) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) - slack-notifier (1.2.1) + slack-notifier (1.5.1) slop (3.6.0) spinach (0.8.10) colorize @@ -722,11 +776,10 @@ GEM tilt (2.0.5) timecop (0.8.1) timfel-krb5-auth (0.8.3) + tool (0.2.3) truncato (0.7.8) htmlentities (~> 4.3.1) nokogiri (~> 1.6.1) - turbolinks (2.5.3) - coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) u2f (0.2.1) @@ -751,7 +804,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - vmstat (2.2.0) + vmstat (2.3.0) warden (1.2.6) rack (>= 1.0) web-console (2.3.0) @@ -787,6 +840,7 @@ DEPENDENCIES allocations (~> 1.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) + asciidoctor-plantuml (= 0.0.6) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) @@ -817,13 +871,14 @@ DEPENDENCIES diffy (~> 3.1.0) doorkeeper (~> 4.2.0) dropzonejs-rails (~> 0.7.1) - email_reply_parser (~> 0.5.8) + email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) factory_girl_rails (~> 4.7.0) - ffaker (~> 2.0.0) + ffaker (~> 2.4) flay (~> 2.6.1) fog-aws (~> 0.9) fog-core (~> 1.40) + fog-google (~> 0.5) fog-local (~> 0.3) fog-openstack (~> 0.1) fog-rackspace (~> 0.1.1) @@ -834,13 +889,14 @@ DEPENDENCIES gemojione (~> 3.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab-markup (~> 1.5.0) - gitlab_git (~> 10.7.0) + gitlab-markup (~> 1.5.1) + gitlab-turbolinks-classic (~> 2.5, >= 2.5.6) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) - grape (~> 0.15.0) + google-api-client (~> 0.8.6) + grape (~> 0.18.0) grape-entity (~> 0.6.0) haml_lint (~> 0.18.2) hamlit (~> 2.6.1) @@ -853,12 +909,12 @@ DEPENDENCIES jira-ruby (~> 1.1.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) json-schema (~> 2.6.2) jwt kaminari (~> 0.17.0) knapsack (~> 1.11.0) + kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) license_finder (~> 2.1.0) licensee (~> 8.0.0) @@ -872,12 +928,12 @@ DEPENDENCIES newrelic_rpm (~> 3.16) nokogiri (~> 1.6.7, >= 1.6.7.2) oauth2 (~> 1.2.0) - octokit (~> 4.3.0) + octokit (~> 4.6.2) oj (~> 2.17.4) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) + omniauth-authentiq (~> 0.2.0) omniauth-azure-oauth2 (~> 0.0.6) - omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) @@ -909,18 +965,18 @@ DEPENDENCIES redis-namespace (~> 1.5.2) redis-rails (~> 5.0.1) request_store (~> 1.3) - rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) - rubocop (~> 0.43.0) - rubocop-rspec (~> 1.5.0) + rubocop (~> 0.46.0) + rubocop-rspec (~> 1.9.1) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.16.2) + rugged (~> 0.24.0) sanitize (~> 2.0) - sass-rails (~> 5.0.6) + sassc-rails (~> 1.3.0) scss_lint (~> 0.47.0) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) @@ -932,7 +988,7 @@ DEPENDENCIES sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (= 0.12.0) - slack-notifier (~> 1.2.0) + slack-notifier (~> 1.5.1) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) spring (~> 1.7.0) @@ -950,7 +1006,6 @@ DEPENDENCIES thin (~> 1.7.0) timecop (~> 0.8.0) truncato (~> 0.7.8) - turbolinks (~> 2.5.0) u2f (~> 0.2.1) uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) @@ -959,10 +1014,10 @@ DEPENDENCIES unicorn-worker-killer (~> 0.4.4) version_sorter (~> 2.1.0) virtus (~> 1.0.1) - vmstat (~> 2.2) + vmstat (~> 2.3.0) web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) BUNDLED WITH - 1.13.6 + 1.13.7 diff --git a/PROCESS.md b/PROCESS.md index 8af660fbdd156c163f0cbc06e76cc8bba6619e24..cbeb781cd3ce31552e0e24935f58a487f00b3ba9 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -69,7 +69,8 @@ to add details to the issue. - ~UX needs help from a UX designer - ~Frontend needs help from a Front-end engineer. Please follow the ["Implement design & UI elements" guidelines]. -- ~up-for-grabs is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels. +- ~"Accepting Merge Requests" is a low priority, well-defined issue that we + encourage people to contribute to. Not exclusive with other labels. - ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote in support or comment for further detail. Do not use `feature request`. - ~bug is an issue reporting undesirable or incorrect behavior. diff --git a/README.md b/README.md index 68b709b85d505565d54c7e4ee57972be71c63f92..4e28f3aacfd69a59c8de9d26944f6f487a95c574 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ To see how GitLab looks please see the [features page on our website](https://ab - Manage Git repositories with fine grained access controls that keep your code secure - Perform code reviews and enhance collaboration with merge requests -- Each project can also have an issue tracker and a wiki +- Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications +- Each project can also have an issue tracker, issue board, and a wiki - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Completely free and open source (MIT Expat license) -- Powered by [Ruby on Rails](https://github.com/rails/rails) ## Hiring @@ -74,11 +74,11 @@ Instructions on how to start GitLab and how to run the tests can be found in the GitLab is a Ruby on Rails application that runs on the following software: -- Ubuntu/Debian/CentOS/RHEL +- Ubuntu/Debian/CentOS/RHEL/OpenSUSE - Ruby (MRI) 2.3 - Git 2.8.4+ - Redis 2.8+ -- MySQL or PostgreSQL +- PostgreSQL (preferred) or MySQL For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html). diff --git a/VERSION b/VERSION index d59bc5cbc5cebd6f8eb698dcb95d685c9a894701..8e9258150a94805cf1980413fdb2d9aaf3dd3f55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.15.0-pre +8.16.0-pre diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png new file mode 100644 index 0000000000000000000000000000000000000000..81767bbcc54f114ec3d6436d3abf640f260dc169 Binary files /dev/null and b/app/assets/images/auth_buttons/authentiq_64.png differ diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 index 82e526ae0ef44af8b276eddc133c7aa350e4cff7..8a260aae1b15de8ae6c4ae993a41a8025562f824 100644 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ b/app/assets/javascripts/abuse_reports.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign */ + ((global) => { const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 31852e4750c1475d3e574cb66dba818f52f31b37..5a7d823e84c8a1d766634d55f999389dd4fb460b 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */ +/* global Turbolinks */ + (function() { this.Admin = (function() { function Admin() { diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1c625e2f2b10aa50aecb5eb51ca8fa7a25baadde..f60f27d1210937aa5a3c405b839b80dfe9dfc2ef 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, no-undef, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ + (function() { - this.Api = { + var Api = { groupsPath: "/api/:version/groups.json", groupPath: "/api/:version/groups/:id.json", namespacesPath: "/api/:version/namespaces.json", @@ -10,6 +11,7 @@ licensePath: "/api/:version/templates/licenses/:key", gitignorePath: "/api/:version/templates/gitignores/:key", gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", + dockerfilePath: "/api/:version/dockerfiles/:key", issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", group: function(group_id, callback) { var url = Api.buildUrl(Api.groupPath) @@ -119,6 +121,10 @@ return callback(file); }); }, + dockerfileYml: function(key, callback) { + var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); + $.get(url, callback); + }, issueTemplate: function(namespacePath, projectPath, key, type, callback) { var url = Api.buildUrl(Api.issuableTemplatePath) .replace(':key', key) @@ -140,4 +146,5 @@ } }; + window.Api = Api; }).call(this); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b7c4673c8e39f8a8dacc2a6e88ab15e0c71e9634..f0615481ed2cd18091b49c927101c5b7ddf82ec9 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,4 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, no-undef, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* global bp */ +/* global Cookies */ +/* global Flash */ +/* global ConfirmDangerModal */ +/* global AwardsHandler */ +/* global Aside */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js @@ -51,6 +58,7 @@ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ /*= require_directory ./u2f */ +/*= require_directory ./droplab */ /*= require_directory . */ /*= require fuzzaldrin-plus */ /*= require es6-promise.auto */ @@ -82,6 +90,14 @@ // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; + // `hashchange` is not triggered when link target is already in window.location + $body.on('click', 'a[href^="#"]', function() { + var href = this.getAttribute('href'); + if (href.substr(1) === gl.utils.getLocationHash()) { + setTimeout(gl.utils.handleLocationHash, 1); + } + }); + // prevent default action for disabled buttons $('.btn').click(function(e) { if ($(this).hasClass('disabled')) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index f4302e2e9f64cd64c3602917b64b0a8938c881d6..107a7978a87d462ec7e59befd7254a0ef0410b86 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-undef, no-plusplus, no-return-assign, camelcase, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */ +/* global Cookies */ + (function() { this.AwardsHandler = (function() { var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index a5d62f881fea0c6a683359eb937fd20e96bfa355..c62a4c5a45658c17dab95ef88124de98635f8674 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */ +/* global autosize */ /*= require jquery.ba-resize */ /*= require autosize */ diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 4edcaa15fe5289a554bd3d8f18668f938e59fddf..586f941a6e35f6cf2734f5025462db5a8dfaabfd 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, camelcase, max-len, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ + // Quick Submit behavior // // When a child field of a form with a `js-quick-submit` class receives a diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index 37531aaec9b49c89e0b9586e1330660567a829ea..57bd13eecf8aeaa07ad17e8f358790d9c5f895f1 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Api */ + /*= require blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..bdf9501761366aaf3c297a2b5f68059ca4abcf16 --- /dev/null +++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 @@ -0,0 +1,18 @@ +/* global Api */ +/*= require blob/template_selector */ + +(() => { + const global = window.gl || (window.gl = {}); + + class BlobDockerfileSelector extends gl.TemplateSelector { + requestFile(query) { + return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this)); + } + + requestFileSuccess(file) { + return super.requestFileSuccess(file); + } + } + + global.BlobDockerfileSelector = BlobDockerfileSelector; +})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..9cee79fa5d56756846ab5c01cbe46f33d1e887f7 --- /dev/null +++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 @@ -0,0 +1,27 @@ +(() => { + const global = window.gl || (window.gl = {}); + + class BlobDockerfileSelectors { + constructor({ editor, $dropdowns } = {}) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new gl.BlobDockerfileSelector({ + editor, + pattern: /(Dockerfile)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), + dropdown: $dropdown, + }); + }); + } + } + + global.BlobDockerfileSelectors = BlobDockerfileSelectors; +})(); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index e0a2e8ac12e91227b396afd5be9e93cfcd9fb235..eab686c45c3e99ead7b063106403d46ad3cd0a89 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, no-undef, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */ +/* global Dropzone */ + (function() { this.BlobFileDropzone = (function() { function BlobFileDropzone(form, method) { diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 7e8f1062ab3ad11e9d73b5947c3419838911620f..15563e429a0f65cbe713e6e2ac4e270f8549dabd 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */ +/* global Api */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index 9a694daa010d90509cd989efcbf55b2100e1ad33..d7f950936881d4e973ea621b80042501a28db30e 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, no-undef, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */ +/* global BlobGitignoreSelector */ + (function() { this.BlobGitignoreSelectors = (function() { function BlobGitignoreSelectors(opts) { diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 9a77fe35d55292c71622eb5b5ff272c605b02742..d9c6f65a0830394fe137d3d611fe01cb64704f9b 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */ +/* global Api */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 index adeb8ba1318500e3ced0e4f05d620fb7a8f2858b..268640681d42dc3bb0797875a524299906b622e9 100644 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */ +/* global BlobLicenseSelector */ + ((global) => { class BlobLicenseSelectors { constructor({ $dropdowns, editor }) { diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 0ff5c0fab057ab95438e20d4a753ece248edad19..7a1ee9998c8542fc9fd85759b4a6376f16f15ae3 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */ + ((global) => { class TemplateSelector { constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index b8eb0f60a8eb1a50548f38455a5e12b9e55e4ff4..8c40e36a80a1a8750b30261483396564c026a8cf 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-undef, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */ +/* global EditBlob */ +/* global NewCommitForm */ + /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 0c74aaaa8525da67113fd84dd387f07db12fe515..fa43ff611ccbb566abf20d26b16015a7d5502ebf 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, no-undef, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */ +/* global ace */ +/* global BlobGitignoreSelectors */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -33,6 +36,9 @@ new gl.BlobCiYamlSelectors({ editor: this.editor }); + new gl.BlobDockerfileSelectors({ + editor: this.editor + }); } EditBlob.prototype.initModePanesAndLinks = function() { @@ -57,7 +63,7 @@ content: this.editor.getValue() }, function(response) { currentPane.empty().append(response); - return currentPane.syntaxHighlight(); + return currentPane.renderGFM(); }); } else { this.$toggleButton.show(); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 7ba918a05f89ef021f5021b01761e7acb34f7c56..2c9ab61c94d533375e1f675f296645e9494fce78 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */ +/* global Vue */ +/* global BoardService */ + //= require vue //= require vue-resource //= require Sortable @@ -70,7 +73,7 @@ $(() => { }); gl.IssueBoardsSearch = new Vue({ - el: '#js-boards-seach', + el: '#js-boards-search', data: { filters: Store.state.filters }, diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 31de3b2528478bbcf7d50f05e725cff5b20256e7..8d1a0f482f9b8e0a68e98421cdffc7bf9daf37c6 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */ +/* global Vue */ +/* global Sortable */ + //= require ./board_blank_state //= require ./board_delete //= require ./board_list @@ -42,14 +45,28 @@ const issue = this.list.findIssue(this.detailIssue.issue.id); if (issue) { + const offsetLeft = this.$el.offsetLeft; const boardsList = document.querySelectorAll('.boards-list')[0]; - const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth; - const left = boardsList.scrollLeft - this.$el.offsetLeft; + const left = boardsList.scrollLeft - offsetLeft; + let right = (offsetLeft + this.$el.offsetWidth); + + if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { + // -290 here because width of boardsList is animating so therefore + // getting the width here is incorrect + // 290 is the width of the sidebar + right -= (boardsList.offsetWidth - 290); + } else { + right -= boardsList.offsetWidth; + } if (right - boardsList.scrollLeft > 0) { - boardsList.scrollLeft = right; + $(boardsList).animate({ + scrollLeft: right + }, this.sortableOptions.animation); } else if (left > 0) { - boardsList.scrollLeft = this.$el.offsetLeft; + $(boardsList).animate({ + scrollLeft: offsetLeft + }, this.sortableOptions.animation); } } }, @@ -62,7 +79,7 @@ } }, mounted () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ + this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, group: 'boards', draggable: '.is-draggable', @@ -81,7 +98,7 @@ } }); - this.sortable = Sortable.create(this.$el.parentNode, options); + this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); }, }); })(); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 index 691487b272ac8ed00df06b13ae3f6f86a5d55521..0a47a22fad27ff317d7b68f65637b90c381b0e29 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, semi */ +/* global Vue */ +/* global ListLabel */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 2299dafd21767f3daf081255810feecf255526ba..5fc5028081177b09657466ba57d341f3eb71114c 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +/* global Vue */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 index c45e1926c5ca6f87bc7afff6175b6323824365ce..861600424a59aa7dddf4a73977cd84e46c12a740 100644 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ b/app/assets/javascripts/boards/components/board_delete.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +/* global Vue */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 43ebeef39c4898abc57788be94920cd965d00a28..6711930622b18e6e109ae78a81f9736fd8163be0 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */ +/* global Vue */ +/* global Sortable */ + //= require ./board_card //= require ./board_new_issue diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index a7989a2ff4c59cf1b8e3d48006b91970a46123d4..2386d3a613cee1e61008e37f170a63f569b9246d 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ +/* global Vue */ +/* global ListIssue */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 1644a7727378f369b57963d0233ec3c90e37c010..02459722bbfbc4cc669e465af4f1b31836216ef9 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-new */ +/* global Vue */ +/* global IssuableContext */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global Sidebar */ + (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 index 10ce746deb59b8b6d7116a48d42cae1e0ef5aca6..3f5cf8420a8593e88e637d08f2f5fc5986ea0509 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -45,10 +46,10 @@ return $li.append($a.prepend($labelColor)); }, - search: { - fields: ['title'] - }, - filterable: true, + search: { + fields: ['title'] + }, + filterable: true, selectable: true, multiSelect: true, clicked (label, $el, e) { diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 9eceac4edddebefa9b8c30e02f57e74e9c8c0cd9..7e192e90fe64ab430662cf6b3915060a0067f96c 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* global Vue */ + Vue.filter('due-date', (value) => { const date = new Date(value); return $.datepicker.formatDate('M d, yy', date); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index 5f99de39122cdebe97d99740d079a88dd6906f2d..d5859444a32a38205e0d56f825a81e9ce3c15ce8 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */ +/* global DocumentTouch */ + ((w) => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -18,6 +20,7 @@ gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { let defaultSortOptions = { + animation: 200, forceFallback: true, fallbackClass: 'is-dragging', fallbackOnBody: true, diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 21d735e8231e6e5ec0c81b9d250f1b5bc68f13b4..cd942c8332b593188954359a9dcf08de92bcfd1e 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */ +/* global Vue */ +/* global ListLabel */ +/* global ListMilestone */ +/* global ListUser */ + class ListIssue { constructor (obj) { this.id = obj.iid; @@ -66,3 +71,5 @@ class ListIssue { return Vue.http.patch(url, data); } } + +window.ListIssue = ListIssue; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index 0910fe9a8540800646b377c372bc5dc5cb5e16aa..9af88d167d68688f4da39384e07a4a681b784e57 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren */ + class ListLabel { constructor (obj) { this.id = obj.id; @@ -9,3 +10,5 @@ class ListLabel { this.priority = (obj.priority !== null) ? obj.priority : Infinity; } } + +window.ListLabel = ListLabel; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 429bd27c3fbd3ae19561e087bdea6fe75b09c167..fb13f529b11fafd2b5a9dcfc3781648ad6677ef6 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */ +/* global ListIssue */ +/* global ListLabel */ + class List { constructor (obj) { this.id = obj.id; @@ -145,3 +148,5 @@ class List { }); } } + +window.List = List; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 index a48969e19c93c5140c28ef90d4e119cfb17b90a8..c867b06d320deab9fc747cd9060e03671cd986a3 100644 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ b/app/assets/javascripts/boards/models/milestone.js.es6 @@ -1,7 +1,10 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListMilestone { - constructor (obj) { + constructor(obj) { this.id = obj.id; this.title = obj.title; } } + +window.ListMilestone = ListMilestone; diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 index 583a973fc4676fe4dc523eed9aaff0cac0ecf175..8e9de4d4cbb69132af385f6f150f4e0930a931b1 100644 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ b/app/assets/javascripts/boards/models/user.js.es6 @@ -1,9 +1,12 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListUser { - constructor (user) { + constructor(user) { this.id = user.id; this.name = user.name; this.username = user.username; this.avatar = user.avatar_url; } } + +window.ListUser = ListUser; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index f59a2ed7937a2eda7f378fbfd3689e965c1e98d1..f18633f091397b604a553439dfb460307a8cc656 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */ +/* global Vue */ + class BoardService { constructor (root, boardId) { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { @@ -63,4 +65,6 @@ class BoardService { issue }); } -}; +} + +window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index bb2a4de8b18b05306504b7172ce8d0ad1e21e73a..e7a14ea5bcaf3bf3cb9e723849cfd515fd6b40cf 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */ +/* global Cookies */ +/* global List */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index 80f137ca12ea87a7f9704e67db61f113faf14534..3723a2039f90c7379d6c113213ee82983d4b0e11 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */ +/* global Vue */ + Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index e7ceb602601e022f0b65706a023ed2a4c6dcfb22..a7e72430141920da250a0cc00d33c61abc2067dd 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */ + (function() { - this.Breakpoints = (function() { + var Breakpoints = (function() { var BreakpointInstance, instance; function Breakpoints() {} @@ -68,4 +69,5 @@ }; })(this)); + window.Breakpoints = Breakpoints; }).call(this); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 116a47b090722e7f42acf63e3ec32445ef84990a..fca470028705f0636a5034fcb9cce803d8fdacfd 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,6 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, no-undef, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */ +/* global Breakpoints */ +/* global Turbolinks */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var AUTO_SCROLL_OFFSET = 75; + var DOWN_BUILD_TRACE = '#down-build-trace'; this.Build = (function() { Build.interval = null; @@ -16,6 +21,17 @@ this.buildStage = options.buildStage; this.updateDropdown = bind(this.updateDropdown, this); this.$document = $(document); + this.$body = $('body'); + this.$buildTrace = $('#build-trace'); + this.$autoScrollContainer = $('.autoscroll-container'); + this.$autoScrollStatus = $('#autoscroll-status'); + this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); + this.$upBuildTrace = $('#up-build-trace'); + this.$downBuildTrace = $(DOWN_BUILD_TRACE); + this.$scrollTopBtn = $('#scroll-top'); + this.$scrollBottomBtn = $('#scroll-bottom'); + this.$buildRefreshAnimation = $('.js-build-refresh'); + clearInterval(Build.interval); // Init breakpoint checker this.bp = Breakpoints.get(); @@ -29,6 +45,7 @@ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); + this.$document.on('scroll', this.initScrollMonitor.bind(this)); $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); this.updateArtifactRemoveDate(); @@ -37,18 +54,6 @@ this.initScrollButtonAffix(); } if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Bind autoscroll button to follow build output - $('#autoscroll-button').on('click', function() { - var state; - state = $(this).data("state"); - if ("enabled" === state) { - $(this).data("state", "disabled"); - return $(this).text("Enable autoscroll"); - } else { - $(this).data("state", "enabled"); - return $(this).text("Disable autoscroll"); - } - }); Build.interval = setInterval((function(_this) { // Check for new build output if user still watching build page // Only valid for runnig build when output changes during time @@ -87,10 +92,14 @@ dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); + if (window.location.hash === DOWN_BUILD_TRACE) { + $("html,body").scrollTop(this.$buildTrace.height()); + } if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { - return $('.js-build-refresh').remove(); + this.$buildRefreshAnimation.remove(); + return this.initScrollMonitor(); } - } + }.bind(this) }); }; @@ -100,6 +109,8 @@ dataType: "json", success: (function(_this) { return function(log) { + var pageUrl; + if (log.state) { _this.state = log.state; } @@ -111,7 +122,12 @@ } return _this.checkAutoscroll(); } else if (log.status !== _this.buildStatus) { - return Turbolinks.visit(_this.pageUrl); + pageUrl = _this.pageUrl; + if (_this.$autoScrollStatus.data('state') === 'enabled') { + pageUrl += DOWN_BUILD_TRACE; + } + + return Turbolinks.visit(pageUrl); } }; })(this) @@ -119,22 +135,95 @@ }; Build.prototype.checkAutoscroll = function() { - if ("enabled" === $("#autoscroll-button").data("state")) { - return $("html,body").scrollTop($("#build-trace").height()); + if (this.$autoScrollStatus.data("state") === "enabled") { + return $("html,body").scrollTop(this.$buildTrace.height()); + } + + // Handle a situation where user started new build + // but never scrolled a page + if (!this.$scrollTopBtn.is(':visible') && + !this.$scrollBottomBtn.is(':visible') && + !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + this.$scrollBottomBtn.show(); } }; Build.prototype.initScrollButtonAffix = function() { - var $body, $buildTrace; - $body = $('body'); - $buildTrace = $('#build-trace'); - return this.$buildScroll.affix({ - offset: { - bottom: function() { - return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); - } + // Hide everything initially + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); + this.$autoScrollContainer.hide(); + } + + // Page scroll listener to detect if user has scrolling page + // and handle following cases + // 1) User is at Top of Build Log; + // - Hide Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + // 2) User is at Bottom of Build Log; + // - Show Top Arrow button + // - Hide Bottom Arrow button + // - Enable Autoscroll and show indicator (when build is running) + // 3) User is somewhere in middle of Build Log; + // - Show Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + Build.prototype.initScrollMonitor = function() { + if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is somewhere in middle of Build Log + + this.$scrollTopBtn.show(); + + if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed + this.$scrollBottomBtn.show(); + } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) { + this.$scrollBottomBtn.show(); + } else { + this.$scrollBottomBtn.hide(); } - }); + + // Hide Autoscroll Status Indicator + if (this.$scrollBottomBtn.is(':visible')) { + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else { + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); + this.$autoScrollStatusText.addClass('animate'); + } + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is at Top of Build Log + + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.show(); + + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) || + (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { + // User is at Bottom of Build Log + + this.$scrollTopBtn.show(); + this.$scrollBottomBtn.hide(); + + // Show and Reposition Autoscroll Status Indicator + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); + this.$autoScrollStatusText.addClass('animate'); + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // Build Log height is small + + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); + + // Hide Autoscroll Status Indicator + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } + + if (this.buildStatus === "running" || this.buildStatus === "pending") { + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled'); + } }; Build.prototype.shouldHideSidebarForViewport = function() { @@ -193,6 +282,7 @@ }; Build.prototype.stepTrace = function(e) { + var $currentTarget; e.preventDefault(); $currentTarget = $(e.currentTarget); $.scrollTo($currentTarget.attr('href'), { diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 index 0ecd20bc11e10331a2755aae3e972a09f4c455de..993424d422f9a47fdefb5f4b93509ab33d6f7f16 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */ + $(function(){ $('.reveal-variables').off('click').on('click',function(){ $('.js-build').toggle().niceScroll(); diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..56ffaa765a8cb1d5c3bc42c54d93231e3e504fb9 --- /dev/null +++ b/app/assets/javascripts/ci_lint_editor.js.es6 @@ -0,0 +1,18 @@ +(() => { + window.gl = window.gl || {}; + + class CILintEditor { + constructor() { + this.editor = window.ace.edit('ci-editor'); + this.textarea = document.querySelector('#content'); + + this.editor.getSession().setMode('ace/mode/yaml'); + this.editor.on('input', () => { + const content = this.editor.getSession().getValue(); + this.textarea.value = content; + }); + } + } + + gl.CILintEditor = CILintEditor; +})(); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index 67509ea7d91c56cff1f4e445902ce60a6c452476..67b33a4d7ee01ede2fcc3f13c587d72450692d35 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */ +/* global CommitFile */ + (function() { this.Commit = (function() { function Commit() { diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 600bac8834a0a71ded3a5a155421b6e57ed3436e..27512312c7cb79dc89751955ffc3b1a81239e245 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */ +/* global ImageFile */ + (function() { this.CommitFile = (function() { function CommitFile(file) { diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 3627aaf5080e624650ec3157b346f4210f4c0f9a..24a6e4ff0e977855a8df9ee4daadb45ddd7404fd 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* global Pager */ + (function() { this.CommitsList = (function() { function CommitsList() {} @@ -6,8 +8,8 @@ CommitsList.timer = null; CommitsList.init = function(limit) { - $("body").on("click", ".day-commits-table li.commit", function(event) { - if (event.target.nodeName !== "A") { + $("body").on("click", ".day-commits-table li.commit", function(e) { + if (e.target.nodeName !== "A") { location.href = $(this).attr("url"); e.stopPropagation(); return false; diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index bd980f87e7222e78f9e5e761ed250c56a7b3dd5d..251f2ded347f74016b94951caef92108da08de0f 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */ + (function() { this.CompareAutocomplete = (function() { function CompareAutocomplete() { @@ -54,6 +55,13 @@ $('.dropdown-toggle-text', $dropdown).text(text); $dropdownContainer.removeClass('open'); }); + + $dropdownContainer.on('click', '.dropdown-content a', (e) => { + $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); + if ($dropdown.hasClass('has-tooltip')) { + $dropdown.tooltip('fixTitle'); + } + }); }); }; diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index efa228a75d90dd4824052ddd9e0a98b575583ff2..6a13f38588d1a95bbd1308aafd9085014e3a1b97 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* global Clipboard */ /*= require clipboard */ diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index 744aa0afa03e84aa9e0383c795c8c2e9410768dd..947c129d5b5be87b43369d5de4834162f6df32e6 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* global Api */ + (function (w) { class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 9cf33e629589eb3d6b23effcd0c17017c220c4c6..5e1a4c948aa8d3bfcee515561b0e9edbc574e93f 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -20,7 +20,7 @@ .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); - this.highlighSelectedLine(); + this.openAnchoredDiff(); } handleClickUnfold(e) { @@ -61,13 +61,22 @@ $.get(link, params, response => $target.parent().replaceWith(response)); } - openAnchoredDiff(anchoredDiff, cb) { - const diffTitle = $(`#file-path-${anchoredDiff}`); + openAnchoredDiff(cb) { + const locationHash = gl.utils.getLocationHash(); + const anchoredDiff = locationHash && locationHash.split('_')[0]; + + if (!anchoredDiff) return; + + const diffTitle = $(`#${anchoredDiff}`); const diffFile = diffTitle.closest('.diff-file'); const nothingHereBlock = $('.nothing-here-block:visible', diffFile); if (nothingHereBlock.length) { - diffFile.singleFileDiff(true, cb); - } else { + const clickTarget = $('.file-title, .click-to-expand', diffFile); + diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { + this.highlighSelectedLine(); + if (cb) cb(); + }); + } else if (cb) { cb(); } } diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 index 52e2846d2790a4a252246c8b4b5440ebe44a7dcf..c59d3996fab2dfcb5724a0452cd9d391ae70a7b0 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */ +/* global Vue */ +/* global CommentsStore */ + (() => { const CommentAndResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index 983e554b9c1b69853fcfe3ec6f67405dddb03c35..f47867fc3b077a142f12935be20d4b1ac01af812 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,6 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + (() => { - JumpToDiscussion = Vue.extend({ + const JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], props: { discussionId: String diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index 27af9fc96ad2817b490f1568a2d95e534b013949..5852b8bbdb7d085f4a42cbac7c10c49806ba4665 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ +/* global Flash */ + (() => { const ResolveBtn = Vue.extend({ props: { @@ -54,9 +59,11 @@ }, methods: { updateTooltip: function () { - $(this.$refs.button) - .tooltip('hide') - .tooltip('fixTitle'); + this.$nextTick(() => { + $(this.$refs.button) + .tooltip('hide') + .tooltip('fixTitle'); + }); }, resolve: function () { if (!this.canResolve) return; @@ -85,7 +92,7 @@ new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); } - this.$nextTick(this.updateTooltip); + this.updateTooltip(); }); } }, diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index 9522ccb49da735a9f0eeeec20e28e826c7810b4a..72cdae812bc47763629894500b61ba19b736f957 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + ((w) => { w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index b945a09fcbe2e94a87cc4c7706e341c24a3648f6..ee5f62b2d9e2545d9a34eea2fc39582b9eefa52d 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ + (() => { const ResolveDiscussionBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index bd4c20aed8badb741ace94011c604bcd46e827c3..1b3a57d0962f370c590043e2491c4e1b06269721 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ -//= require vue -//= require vue-resource +/* eslint-disable func-names, comma-dangle, new-cap, no-new */ +/* global Vue */ +/* global ResolveCount */ + //= require_directory ./models //= require_directory ./stores //= require_directory ./services diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 index 7a929017f36a00e706c98262a52273d1b95a3f5c..a9ea18bf82b870031604513a42ba6053edaa7292 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */ + ((w) => { w.DiscussionMixins = { computed: { diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 index badcdccc840138200076c8e561e6620616c534a4..fa518ba4d33a8b47c8d6f40b073e59538565c155 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ +/* global Vue */ +/* global NoteModel */ + class DiscussionModel { constructor (discussionId) { this.id = discussionId; @@ -69,7 +72,7 @@ class DiscussionModel { gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { - $discussionHeadline.remove(); + $discussionHeadline.remove(); } } @@ -89,3 +92,5 @@ class DiscussionModel { return false; } } + +window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 index d0541b0263278efe32e105b30d6312b26148f770..f3a7cba5ef6bb331a14d595a9475971435c2ad4d 100644 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ b/app/assets/javascripts/diff_notes/models/note.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable camelcase, no-unused-vars */ + class NoteModel { - constructor (discussionId, noteId, canResolve, resolved, resolved_by) { + constructor(discussionId, noteId, canResolve, resolved, resolved_by) { this.discussionId = discussionId; this.id = noteId; this.canResolve = canResolve; @@ -8,3 +9,5 @@ class NoteModel { this.resolved_by = resolved_by; } } + +window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 86953ce7ffb2df158bfc262d4c3f83a284b16861..78c74985f78589dfd95e1bc66099e65005b0e7f2 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */ +/* global Vue */ +/* global Flash */ +/* global CommentsStore */ + ((w) => { class ResolveServiceClass { constructor() { diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 index f42ca406bb1df2779b5a761043dcedd7e9495cbc..1a7abbc6f75ceb8ae1bbbcdd74a69a91676315ab 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* global Vue */ +/* global DiscussionModel */ + ((w) => { w.CommentsStore = { state: {}, diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 413117c22268317a4b4ba9fdff2832b55a061545..99a34651639b08dcf7852c6ae5bfd948947de213 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,4 +1,42 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */ +/* global UsernameValidator */ +/* global ActiveTabMemoizer */ +/* global ShortcutsNavigation */ +/* global Build */ +/* global Issuable */ +/* global Issue */ +/* global ShortcutsIssuable */ +/* global ZenMode */ +/* global Milestone */ +/* global GLForm */ +/* global IssuableForm */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global MergedButtons */ +/* global Commit */ +/* global NotificationsForm */ +/* global TreeView */ +/* global NotificationsDropdown */ +/* global UsersSelect */ +/* global GroupAvatar */ +/* global LineHighlighter */ +/* global ShortcutsBlob */ +/* global ProjectFork */ +/* global BuildArtifacts */ +/* global GroupsSelect */ +/* global Search */ +/* global Admin */ +/* global NamespaceSelects */ +/* global ShortcutsDashboardNavigation */ +/* global Project */ +/* global ProjectAvatar */ +/* global CompareAutocomplete */ +/* global ProjectNew */ +/* global Star */ +/* global ProjectShow */ +/* global Labels */ +/* global Shortcuts */ + (function() { var Dispatcher; @@ -26,6 +64,17 @@ new UsernameValidator(); new ActiveTabMemoizer(); break; + case 'sessions:create': + if (!gon.u2f) break; + window.gl.u2fAuthenticate = new gl.U2FAuthenticate( + $("#js-authenticate-u2f"), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + window.gl.u2fAuthenticate.start(); + break; case 'projects:boards:show': case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); @@ -35,8 +84,13 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } Issuable.init(); - new gl.IssuableBulkActions(); + new gl.IssuableBulkActions({ + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', + }); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': @@ -98,7 +152,6 @@ new MergedButtons(); break; case 'projects:merge_requests:commits': - case 'projects:merge_requests:builds': new MergedButtons(); break; case "projects:merge_requests:diffs": @@ -106,10 +159,6 @@ new ZenMode(); new MergedButtons(); break; - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': new gl.Activities(); break; @@ -122,8 +171,10 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; - case 'projects:commit:builds': - new gl.Pipelines(); + case 'projects:commit:pipelines': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); break; case 'projects:commits:show': case 'projects:activity': @@ -162,7 +213,9 @@ new gl.Members(); new UsersSelect(); break; - case 'projects:project_members:index': + case 'projects:members:show': + new gl.MemberExpirationDate('.js-access-expiration-date-groups'); + new GroupsSelect(); new gl.MemberExpirationDate(); new gl.Members(); new UsersSelect(); @@ -208,10 +261,6 @@ case 'projects:artifacts:browse': new BuildArtifacts(); break; - case 'projects:group_links:index': - new gl.MemberExpirationDate(); - new GroupsSelect(); - break; case 'search:show': new Search(); break; @@ -222,6 +271,10 @@ case 'projects:variables:index': new gl.ProjectVariables(); break; + case 'ci:lints:create': + case 'ci:lints:show': + new gl.CILintEditor(); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js new file mode 100644 index 0000000000000000000000000000000000000000..ed545ec8748629a54cbb66e68aa2aaec91b8aed2 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab.js @@ -0,0 +1,701 @@ +/* eslint-disable */ +// Determine where to place this +if (typeof Object.assign != 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +var DATA_TRIGGER = 'data-dropdown-trigger'; +var DATA_DROPDOWN = 'data-dropdown'; + +module.exports = { + DATA_TRIGGER: DATA_TRIGGER, + DATA_DROPDOWN: DATA_DROPDOWN, +} + +},{}],2:[function(require,module,exports){ +// Custom event support for IE +if ( typeof CustomEvent === "function" ) { + module.exports = CustomEvent; +} else { + require('./window')(function(w){ + var CustomEvent = function ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + CustomEvent.prototype = w.Event.prototype; + + w.CustomEvent = CustomEvent; + }); + module.exports = CustomEvent; +} + +},{"./window":11}],3:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var utils = require('./utils'); + +var DropDown = function(list) { + this.hidden = true; + this.list = list; + this.items = []; + this.getItems(); + this.addEvents(); + this.initialState = list.innerHTML; +}; + +Object.assign(DropDown.prototype, { + getItems: function() { + this.items = [].slice.call(this.list.querySelectorAll('li')); + return this.items; + }, + + clickEvent: function(e) { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); + + if(selected) { + e.preventDefault(); + this.hide(); + var listEvent = new CustomEvent('click.dl', { + detail: { + list: this, + selected: selected, + data: e.target.dataset, + }, + }); + this.list.dispatchEvent(listEvent); + } + }, + + addEvents: function() { + this.clickWrapper = this.clickEvent.bind(this); + // event delegation. + this.list.addEventListener('click', this.clickWrapper); + }, + + toggle: function() { + if(this.hidden) { + this.show(); + } else { + this.hide(); + } + }, + + setData: function(data) { + this.data = data; + this.render(data); + }, + + addData: function(data) { + this.data = (this.data || []).concat(data); + this.render(data); + }, + + // call render manually on data; + render: function(data){ + // debugger + // empty the list first + var sampleItem; + var newChildren = []; + var toAppend; + + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + sampleItem = item; + if(item.parentNode && item.parentNode.dataset.hasOwnProperty('dynamic')) { + item.parentNode.removeChild(item); + } + } + + newChildren = this.data.map(function(dat){ + var html = utils.t(sampleItem.outerHTML, dat); + var template = document.createElement('div'); + template.innerHTML = html; + // console.log(template.content) + + // Help set the image src template + var imageTags = template.querySelectorAll('img[data-src]'); + // debugger + for(var i = 0; i < imageTags.length; i++) { + var imageTag = imageTags[i]; + imageTag.src = imageTag.getAttribute('data-src'); + imageTag.removeAttribute('data-src'); + } + + if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){ + template.firstChild.style.display = 'none' + }else{ + template.firstChild.style.display = 'block'; + } + return template.firstChild.outerHTML; + }); + toAppend = this.list.querySelector('ul[data-dynamic]'); + if(toAppend) { + toAppend.innerHTML = newChildren.join(''); + } else { + this.list.innerHTML = newChildren.join(''); + } + }, + + show: function() { + // debugger + this.list.style.display = 'block'; + this.hidden = false; + }, + + hide: function() { + // debugger + this.list.style.display = 'none'; + this.hidden = true; + }, + + destroy: function() { + if (!this.hidden) { + this.hide(); + } + + this.list.removeEventListener('click', this.clickWrapper); + } +}); + +module.exports = DropDown; + +},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){ +require('./window')(function(w){ + module.exports = function(deps) { + deps = deps || {}; + var window = deps.window || w; + var document = deps.document || window.document; + var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill'); + var HookButton = deps.HookButton || require('./hook_button'); + var HookInput = deps.HookInput || require('./hook_input'); + var utils = deps.utils || require('./utils'); + var DATA_TRIGGER = require('./constants').DATA_TRIGGER; + + var DropLab = function(hook){ + if (!(this instanceof DropLab)) return new DropLab(hook); + this.ready = false; + this.hooks = []; + this.queuedData = []; + this.config = {}; + this.loadWrapper; + if(typeof hook !== 'undefined'){ + this.addHook(hook); + } + }; + + + Object.assign(DropLab.prototype, { + load: function() { + this.loadWrapper(); + }, + + loadWrapper: function(){ + var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); + this.addHooks(dropdownTriggers).init(); + }, + + addData: function () { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_addData'); + }, + + setData: function() { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_setData'); + }, + + destroy: function() { + for(var i = 0; i < this.hooks.length; i++) { + this.hooks[i].destroy(); + } + this.hooks = []; + this.removeEvents(); + }, + + applyArgs: function(args, methodName) { + if(this.ready) { + this[methodName].apply(this, args); + } else { + this.queuedData = this.queuedData || []; + this.queuedData.push(args); + } + }, + + _addData: function(trigger, data) { + this._processData(trigger, data, 'addData'); + }, + + _setData: function(trigger, data) { + this._processData(trigger, data, 'setData'); + }, + + _processData: function(trigger, data, methodName) { + for(var i = 0; i < this.hooks.length; i++) { + var hook = this.hooks[i]; + if(hook.trigger.dataset.hasOwnProperty('id')) { + if(hook.trigger.dataset.id === trigger) { + hook.list[methodName](data); + } + } + } + }, + + addEvents: function() { + var self = this; + this.windowClickedWrapper = function(e){ + var thisTag = e.target; + if(thisTag.tagName !== 'UL'){ + // climb up the tree to find the UL + thisTag = utils.closest(thisTag, 'UL'); + } + if(utils.isDropDownParts(thisTag)){ return } + if(utils.isDropDownParts(e.target)){ return } + for(var i = 0; i < self.hooks.length; i++) { + self.hooks[i].list.hide(); + } + }.bind(this); + w.addEventListener('click', this.windowClickedWrapper); + }, + + removeEvents: function(){ + w.removeEventListener('click', this.windowClickedWrapper); + w.removeEventListener('load', this.loadWrapper); + }, + + changeHookList: function(trigger, list, plugins, config) { + trigger = document.querySelector('[data-id="'+trigger+'"]'); + // list = document.querySelector(list); + this.hooks.every(function(hook, i) { + if(hook.trigger === trigger) { + hook.destroy(); + this.hooks.splice(i, 1); + this.addHook(trigger, list, plugins, config); + return false; + } + return true + }.bind(this)); + }, + + addHook: function(hook, list, plugins, config) { + if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ + hook = document.querySelector(hook); + } + if(!list){ + list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); + } + + if(hook) { + if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { + this.hooks.push(new HookButton(hook, list, plugins, config)); + } else if(hook.tagName === 'INPUT') { + this.hooks.push(new HookInput(hook, list, plugins, config)); + } + } + return this; + }, + + addHooks: function(hooks, plugins, config) { + for(var i = 0; i < hooks.length; i++) { + var hook = hooks[i]; + this.addHook(hook, null, plugins, config); + } + return this; + }, + + setConfig: function(obj){ + this.config = obj; + }, + + init: function () { + this.addEvents(); + var readyEvent = new CustomEvent('ready.dl', { + detail: { + dropdown: this, + }, + }); + window.dispatchEvent(readyEvent); + this.ready = true; + for(var i = 0; i < this.queuedData.length; i++) { + this.addData.apply(this, this.queuedData[i]); + } + this.queuedData = []; + return this; + }, + }); + + return DropLab; + }; +}); + +},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){ +var DropDown = require('./dropdown'); + +var Hook = function(trigger, list, plugins, config){ + this.trigger = trigger; + this.list = new DropDown(list); + this.type = 'Hook'; + this.event = 'click'; + this.plugins = plugins || []; + this.config = config || {}; + this.id = trigger.dataset.id; +}; + +Object.assign(Hook.prototype, { + + addEvents: function(){}, + + constructor: Hook, +}); + +module.exports = Hook; + +},{"./dropdown":3}],6:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var Hook = require('./hook'); + +var HookButton = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); + this.type = 'button'; + this.event = 'click'; + this.addEvents(); + this.addPlugins(); +}; + +HookButton.prototype = Object.create(Hook.prototype); + +Object.assign(HookButton.prototype, { + addPlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].init(this); + } + }, + + clicked: function(e){ + var buttonEvent = new CustomEvent('click.dl', { + detail: { + hook: this, + }, + bubbles: true, + cancelable: true + }); + this.list.show(); + e.target.dispatchEvent(buttonEvent); + }, + + addEvents: function(){ + this.clickedWrapper = this.clicked.bind(this); + this.trigger.addEventListener('click', this.clickedWrapper); + }, + + removeEvents: function(){ + this.trigger.removeEventListener('click', this.clickedWrapper); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].destroy(); + } + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + }, + + + constructor: HookButton, +}); + + +module.exports = HookButton; + +},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var Hook = require('./hook'); + +var HookInput = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); + this.type = 'input'; + this.event = 'input'; + this.addPlugins(); + this.addEvents(); +}; + +Object.assign(HookInput.prototype, { + addPlugins: function() { + var self = this; + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].init(self); + } + }, + + addEvents: function(){ + var self = this; + + this.mousedown = function mousedown(e) { + var mouseEvent = new CustomEvent('mousedown.dl', { + detail: { + hook: self, + text: e.target.value, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(mouseEvent); + } + + this.input = function input(e) { + var inputEvent = new CustomEvent('input.dl', { + detail: { + hook: self, + text: e.target.value, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(inputEvent); + self.list.show(); + } + + this.keyup = function keyup(e) { + keyEvent(e, 'keyup.dl'); + } + + this.keydown = function keydown(e) { + keyEvent(e, 'keydown.dl'); + } + + function keyEvent(e, keyEventName){ + var keyEvent = new CustomEvent(keyEventName, { + detail: { + hook: self, + text: e.target.value, + which: e.which, + key: e.key, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(keyEvent); + self.list.show(); + } + + this.events = this.events || {}; + this.events.mousedown = this.mousedown; + this.events.input = this.input; + this.events.keyup = this.keyup; + this.events.keydown = this.keydown; + this.trigger.addEventListener('mousedown', this.mousedown); + this.trigger.addEventListener('input', this.input); + this.trigger.addEventListener('keyup', this.keyup); + this.trigger.addEventListener('keydown', this.keydown); + }, + + removeEvents: function(){ + this.trigger.removeEventListener('mousedown', this.mousedown); + this.trigger.removeEventListener('input', this.input); + this.trigger.removeEventListener('keyup', this.keyup); + this.trigger.removeEventListener('keydown', this.keydown); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].destroy(); + } + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + this.list.destroy(); + } +}); + +module.exports = HookInput; + +},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){ +var DropLab = require('./droplab')(); +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var keyboard = require('./keyboard')(); +var setup = function() { + window.DropLab = DropLab; +}; + + +module.exports = setup(); + +},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){ +require('./window')(function(w){ + module.exports = function(){ + var currentKey; + var currentFocus; + var currentIndex = 0; + var isUpArrow = false; + var isDownArrow = false; + var removeHighlight = function removeHighlight(list) { + var listItems = list.list.querySelectorAll('li'); + for(var i = 0; i < listItems.length; i++) { + listItems[i].classList.remove('dropdown-active'); + } + return listItems; + }; + + var setMenuForArrows = function setMenuForArrows(list) { + var listItems = removeHighlight(list); + if(currentIndex>0){ + if(!listItems[currentIndex-1]){ + currentIndex = currentIndex-1; + } + listItems[currentIndex-1].classList.add('dropdown-active'); + } + }; + + var mousedown = function mousedown(e) { + var list = e.detail.hook.list; + removeHighlight(list); + list.show(); + currentIndex = 0; + isUpArrow = false; + isDownArrow = false; + }; + var selectItem = function selectItem(list) { + var listItems = removeHighlight(list); + var currentItem = listItems[currentIndex-1]; + var listEvent = new CustomEvent('click.dl', { + detail: { + list: list, + selected: currentItem, + data: currentItem.dataset, + }, + }); + list.list.dispatchEvent(listEvent); + list.hide(); + } + + var keydown = function keydown(e){ + var typedOn = e.target; + isUpArrow = false; + isDownArrow = false; + + if(e.detail.which){ + currentKey = e.detail.which; + if(currentKey === 13){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 38) { + isUpArrow = true; + } + if(currentKey === 40) { + isDownArrow = true; + } + } else if(e.detail.key) { + currentKey = e.detail.key; + if(currentKey === 'Enter'){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 'ArrowUp') { + isUpArrow = true; + } + if(currentKey === 'ArrowDown') { + isDownArrow = true; + } + } + if(isUpArrow){ currentIndex--; } + if(isDownArrow){ currentIndex++; } + if(currentIndex < 0){ currentIndex = 0; } + setMenuForArrows(e.detail.hook.list); + }; + + w.addEventListener('mousedown.dl', mousedown); + w.addEventListener('keydown.dl', keydown); + }; +}); +},{"./window":11}],10:[function(require,module,exports){ +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN; + +var toDataCamelCase = function(attr){ + return this.camelize(attr.split('-').slice(1).join(' ')); +}; + +// the tiniest damn templating I can do +var t = function(s,d){ + for(var p in d) + s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]); + return s; +}; + +var camelize = function(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { + return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); + }).replace(/\s+/g, ''); +}; + +var closest = function(thisTag, stopTag) { + while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + thisTag = thisTag.parentNode; + } + return thisTag; +}; + +var isDropDownParts = function(target) { + if(target.tagName === 'HTML') { return false; } + return ( + target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_DROPDOWN) + ); +}; + +module.exports = { + toDataCamelCase: toDataCamelCase, + t: t, + camelize: camelize, + closest: closest, + isDropDownParts: isDropDownParts, +}; + +},{"./constants":1}],11:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[8])(8) +}); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js new file mode 100644 index 0000000000000000000000000000000000000000..f20610b3811de8fc8cb08a87e711125d17f9e49d --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -0,0 +1,79 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + function droplabAjaxException(message) { + this.message = message; + } + + w.droplabAjax = { + _loadUrlData: function _loadUrlData(url) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + return resolve(data); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + init: function init(hook) { + var self = this; + var config = hook.config.droplabAjax; + + if (!config || !config.endpoint || !config.method) { + return; + } + + if (config.method !== 'setData' && config.method !== 'addData') { + return; + } + + if (config.loadingTemplate) { + var dynamicList = hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', ''); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + + this._loadUrlData(config.endpoint) + .then(function(d) { + if (config.loadingTemplate) { + var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } + } + hook.list[config.method].call(hook.list, d); + }).catch(function(e) { + throw new droplabAjaxException(e.message || e); + }); + }, + + destroy: function() { + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js new file mode 100644 index 0000000000000000000000000000000000000000..af163f7685144a00e981e9b52919193250b4b577 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -0,0 +1,145 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + w.droplabAjaxFilter = { + init: function(hook) { + this.destroyed = false; + this.hook = hook; + this.notLoading(); + + this.debounceTriggerWrapper = this.debounceTrigger.bind(this); + this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); + this.trigger(true); + }, + + notLoading: function notLoading() { + this.loading = false; + }, + + debounceTrigger: function debounceTrigger(e) { + var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; + var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; + var focusEvent = e.type === 'focus'; + + if (invalidKeyPressed || this.loading) { + return; + } + + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); + }, + + trigger: function trigger(getEntireList) { + var config = this.hook.config.droplabAjaxFilter; + var searchValue = this.trigger.value; + + if (!config || !config.endpoint || !config.searchKey) { + return; + } + + if (config.searchValueFunction) { + searchValue = config.searchValueFunction(); + } + + if (config.loadingTemplate && this.hook.list.data === undefined || + this.hook.list.data.length === 0) { + var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + + if (getEntireList) { + searchValue = ''; + } + + if (config.searchKey === searchValue) { + return this.list.show(); + } + + this.loading = true; + + var params = config.params || {}; + params[config.searchKey] = searchValue; + var self = this; + this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { + if (config.loadingTemplate && self.hook.list.data === undefined || + self.hook.list.data.length === 0) { + const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } + } + + if (!self.destroyed) { + var hookListChildren = self.hook.list.list.children; + var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); + + if (onlyDynamicList && data.length === 0) { + self.hook.list.hide(); + } + + self.hook.list.setData.call(self.hook.list, data); + } + self.notLoading(); + }); + }, + + _loadUrlData: function _loadUrlData(url) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + return resolve(data); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + buildParams: function(params) { + if (!params) return ''; + var paramsArray = Object.keys(params).map(function(param) { + return param + '=' + (params[param] || ''); + }); + return '?' + paramsArray.join('&'); + }, + + destroy: function destroy() { + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.destroyed = true; + + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js new file mode 100644 index 0000000000000000000000000000000000000000..41a220831f96f753788238df1e1b7d2cadd866cf --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -0,0 +1,60 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + w.droplabFilter = { + + keydownWrapper: function(e){ + var list = e.detail.hook.list; + var data = list.data; + var value = e.detail.hook.trigger.value.toLowerCase(); + var config = e.detail.hook.config.droplabFilter; + var matches = []; + var filterFunction; + // will only work on dynamically set data + if(!data){ + return; + } + + if (config && config.filterFunction && typeof config.filterFunction === 'function') { + filterFunction = config.filterFunction; + } else { + filterFunction = function(o){ + // cheap string search + o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1; + return o; + }; + } + + matches = data.map(function(o) { + return filterFunction(o, value); + }); + list.render(matches); + }, + + init: function init(hookInput) { + var config = hookInput.config.droplabFilter; + + if (!config || (!config.template && !config.filterFunction)) { + return; + } + + this.hookInput = hookInput; + this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper); + }, + + destroy: function destroy(){ + this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index e1e76bca6ad374f7403f798a9a0bb4905e06182c..56cb39be642d6f116f4b4bab9d5fe63231c1064f 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, no-undef, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */ +/* global Dropzone */ /*= require preview_markdown */ diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index e84f5ac9183f8db1807730b7e330deb01e30c964..201f9fdc3fe8621f0e168dc5f9b4ae632643720c 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */ + (function(global) { class DueDateSelect { constructor({ $dropdown, $loading } = {}) { @@ -79,9 +80,12 @@ } parseSelectedDate() { - this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val(); + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + if (this.rawSelectedDate.length) { - let dateObj = new Date(this.rawSelectedDate); + // Construct Date object manually to avoid buggy dateString support within Date constructor + const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); } else { this.displayedDate = 'No due date'; diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 1db29dd47fb7bde5d496c8c9c058daeb522d5d59..8b6fafb610407fbe46fa3c3f8fe14a12a95363ec 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -18,7 +18,7 @@ * The environments array is a recursive tree structure and we need to filter * both root level environments and children environments. * - * In order to acomplish that, both `filterState` and `filterEnvironmnetsByState` + * In order to acomplish that, both `filterState` and `filterEnvironmentsByState` * functions work together. * The first one works as the filter that verifies if the given environment matches * the given state. @@ -34,9 +34,9 @@ * @param {Array} array * @return {Array} */ - const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => { + const filterEnvironmentsByState = (fn, arr) => arr.map((item) => { if (item.children) { - const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean); + const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean); if (filteredChildren.length) { item.children = filteredChildren; return item; @@ -45,7 +45,7 @@ return fn(item); }).filter(Boolean); - window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { + gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { props: { store: { type: Object, @@ -55,7 +55,7 @@ }, components: { - 'environment-item': window.gl.environmentsList.EnvironmentItem, + 'environment-item': gl.environmentsList.EnvironmentItem, }, data() { @@ -76,12 +76,13 @@ helpPagePath: environmentsData.helpPagePath, commitIconSvg: environmentsData.commitIconSvg, playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, }; }, computed: { filteredEnvironments() { - return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments); + return filterEnvironmentsByState(filterState(this.visibility), this.state.environments); }, scope() { @@ -102,7 +103,7 @@ }, /** - * Fetches all the environmnets and stores them. + * Fetches all the environments and stores them. * Toggles loading property. */ created() { @@ -164,8 +165,7 @@ {{state.availableCounter}} </span> </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> + </li><li v-bind:class="{ 'active' : scope === 'stopped' }"> <a :href="projectStoppedEnvironmentsPath"> Stopped <span class="badge js-stopped-environments-count"> @@ -216,7 +216,7 @@ <th class="environments-deploy">Last deployment</th> <th class="environments-build">Build</th> <th class="environments-commit">Commit</th> - <th class="environments-date"></th> + <th class="environments-date">Created</th> <th class="hidden-xs environments-actions"></th> </tr> </thead> @@ -231,6 +231,7 @@ :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" :commit-icon-svg="commitIconSvg"></tr> <tr v-if="model.isOpen && model.children && model.children.length > 0" @@ -241,6 +242,7 @@ :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" :commit-icon-svg="commitIconSvg"> </tr> diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 7c743705d51f2268c642d244013d4f9a52391f1c..81468f4d3bc04575149a103ed7889cda0766478d 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', { + gl.environmentsList.ActionsComponent = Vue.component('actions-component', { props: { actions: { type: Array, diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index aed65b33c04aa1a30d1c0b5cfa3b52045526f189..6592c1b5f0f20523df4e71fd4e723e564b1489ba 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { + gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { props: { externalUrl: { type: String, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 2e046a601462cf19e6e7a447beeb81a3b5d9bb85..0e6bc3fdb2c95e2a225554a0a86e915c8138e22d 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -8,6 +8,7 @@ /*= require ./environment_external_url */ /*= require ./environment_stop */ /*= require ./environment_rollback */ +/*= require ./environment_terminal_button */ (() => { /** @@ -28,11 +29,12 @@ gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { components: { - 'commit-component': window.gl.CommitComponent, - 'actions-component': window.gl.environmentsList.ActionsComponent, - 'external-url-component': window.gl.environmentsList.ExternalUrlComponent, - 'stop-component': window.gl.environmentsList.StopComponent, - 'rollback-component': window.gl.environmentsList.RollbackComponent, + 'commit-component': gl.CommitComponent, + 'actions-component': gl.environmentsList.ActionsComponent, + 'external-url-component': gl.environmentsList.ExternalUrlComponent, + 'stop-component': gl.environmentsList.StopComponent, + 'rollback-component': gl.environmentsList.RollbackComponent, + 'terminal-button-component': gl.environmentsList.TerminalButtonComponent, }, props: { @@ -68,6 +70,12 @@ type: String, required: false, }, + + terminalIconSvg: { + type: String, + required: false, + }, + }, data() { @@ -175,7 +183,7 @@ * @returns {String} */ createdDate() { - return window.gl.environmentsList.timeagoInstance.format( + return gl.environmentsList.timeagoInstance.format( this.model.last_deployment.deployable.created_at, ); }, @@ -449,7 +457,7 @@ </span> </td> - <td> + <td class="environments-build-cell"> <a v-if="shouldRenderBuildName" class="build-link" :href="model.last_deployment.deployable.build_path"> @@ -506,6 +514,14 @@ </stop-component> </div> + <div v-if="model.terminal_path" + class="inline js-terminal-button-container"> + <terminal-button-component + :terminal-icon-svg="terminalIconSvg" + :terminal-path="model.terminal_path"> + </terminal-button-component> + </div> + <div v-if="canRetry && canCreateDeployment" class="inline js-rollback-component-container"> <rollback-component diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index 6d4e8fad604c0d33079f33b0cf8d32f0bf62e489..b52298b4a88a88740478999960661e5887bdc056 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { + gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { props: { retryUrl: { type: String, diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index 7292f924e5c3822b1243f34d0c0ddb9489d57a4a..0a29f2f36e993baf22128de35aebe507b5158a13 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.StopComponent = Vue.component('stop-component', { + gl.environmentsList.StopComponent = Vue.component('stop-component', { props: { stopUrl: { type: String, diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..050184ba497a3cc5cf6c18c88e58ad5a7d836e26 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -0,0 +1,27 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { + props: { + terminalPath: { + type: String, + default: '', + }, + terminalIconSvg: { + type: String, + default: '', + }, + }, + + template: ` + <a class="btn terminal-button" + :href="terminalPath"> + <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> + </a> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 20eee7976ec826304e745029948b3fc5522e08aa..9f24a6a4f88ac63abd98b7ad7e450a1f08b8b4e9 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -7,15 +7,17 @@ $(() => { window.gl = window.gl || {}; - if (window.gl.EnvironmentsListApp) { - window.gl.EnvironmentsListApp.$destroy(true); + if (gl.EnvironmentsListApp) { + gl.EnvironmentsListApp.$destroy(true); } - const Store = window.gl.environmentsList.EnvironmentsStore; + const Store = gl.environmentsList.EnvironmentsStore; - window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({ + gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), + propsData: { store: Store.create(), }, + }); }); diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 15ec7b76c3d4de998856add6ec9371908b855f3e..575a45d9802ea8001dfd7e3c063ca738879959e7 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -20,3 +20,5 @@ class EnvironmentsService { return this.environments.get(); } } + +window.EnvironmentsService = EnvironmentsService; diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..70a2d765abd04cfdb9e140a1db86dda483c24e4d --- /dev/null +++ b/app/assets/javascripts/extensions/object.js.es6 @@ -0,0 +1,26 @@ +/* eslint-disable no-restricted-syntax */ + +// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +if (typeof Object.assign !== 'function') { + Object.assign = function assign(target, ...args) { + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 0; index < args.length; index += 1) { + const nextSource = args[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (const nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 0122e84716138066f1e70ea6ecce0c1eb092ca2d..785f286997019c307fc5653dc67fdaa2c527f58f 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */ +/* global FilesCommentButton */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..63c20f57520c1cc87a4f5a20e985a981dc4dba24 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -0,0 +1,66 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabFilter */ + +(() => { + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: gl.DropdownUtils.filterHint, + }, + }; + } + + itemClicked(e) { + const { selected } = e.detail; + + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); + } + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + } + + renderContent() { + const dropdownData = [{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<@author>', + }, { + icon: 'fa-user', + hint: 'assignee:', + tag: '<@assignee>', + }, { + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<%milestone>', + }, { + icon: 'fa-tag', + hint: 'label:', + tag: '<~label>', + }]; + + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); + this.droplab.setData(this.hookId, dropdownData); + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..f06c3fc9c6f58d2389aa85c386902585196d70d3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -0,0 +1,44 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjax */ +/* global droplabFilter */ + +(() => { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); + this.symbol = symbol; + this.config = { + droplabAjax: { + endpoint, + method: 'setData', + loadingTemplate: this.loadingTemplate, + }, + droplabFilter: { + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol), + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, (selected) => { + const title = selected.querySelector('.js-data-value').innerText.trim(); + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; + }); + } + + renderContent(forceShowList = false) { + this.droplab + .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); + } + + init() { + this.droplab + .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownNonUser = DropdownNonUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..e80d266ae899b1d829c36cfd04945ecf5bfdedcf --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -0,0 +1,53 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjaxFilter */ + +(() => { + class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, + }, + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + + getSearchInput() { + const query = this.input.value.trim(); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + + return lastToken.value || ''; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..c27ef3042d1bdfdf2f6f082a55be9b6df4f96b5f --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -0,0 +1,79 @@ +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, item, query) { + const updatedItem = item; + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); + + if (lastToken !== searchToken) { + const title = updatedItem.title.toLowerCase(); + let value = lastToken.value.toLowerCase(); + + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } + + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + } else { + updatedItem.droplab_hidden = false; + } + + return updatedItem; + } + + static filterHint(item, query) { + const updatedItem = item; + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + lastToken = lastToken.key || lastToken || ''; + + if (!lastToken || query.split('').last() === ' ') { + updatedItem.droplab_hidden = false; + } else if (lastToken) { + const split = lastToken.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; + } + + return updatedItem; + } + + static setDataValueIfSelected(filter, selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js new file mode 100644 index 0000000000000000000000000000000000000000..d188718c5f3c1de24e9f23f1f43fb82caa0b9292 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -0,0 +1,7 @@ + // This is a manifest file that'll be compiled into including all the files listed below. + // Add new JavaScript code in separate files in this directory and they'll automatically + // be included in the compiled file accessible from http://example.com/assets/application.js + // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the + // the compiled file. + // + /*= require_tree . */ diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..886d8113f4ab2457a9c28499924cc7ecdd4c8e38 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -0,0 +1,102 @@ +(() => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + this.droplab = droplab; + this.hookId = input.getAttribute('data-id'); + this.input = input; + this.filter = filter; + this.dropdown = dropdown; + this.loadingTemplate = `<div class="filter-dropdown-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div>`; + this.bindEvents(); + } + + bindEvents() { + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + } + + itemClicked(e, getValueFunction) { + const { selected } = e.detail; + + if (selected.tagName === 'LI' && selected.innerHTML) { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); + } + + this.dismissDropdown(); + } + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + } + + setOffset(offset = 0) { + this.dropdown.style.left = `${offset}px`; + } + + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } + } + + render(forceRenderContent = false, forceShowList = false) { + this.setAsDropdown(); + + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === null; + + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if (currentHook.list.list.id !== this.dropdown.id) { + this.renderContent(forceShowList); + } + } + + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + + hideDropdown() { + this.getCurrentHook().list.hide(); + } + + resetFilters() { + const hook = this.getCurrentHook(); + const data = hook.list.data; + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); + hook.list.render(results); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..1cd0483877a297f66b9b92552e29a91722d719ad --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -0,0 +1,193 @@ +/* global DropLab */ + +(() => { + class FilteredSearchDropdownManager { + constructor() { + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + + this.setupMapping(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + }; + } + + static addWordToInput(tokenName, tokenValue = '') { + const input = document.querySelector('.filtered-search'); + const word = `${tokenName}:${tokenValue}`; + + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value); + const lastSearchToken = searchToken.split(' ').last(); + const lastInputCharacter = input.value[input.value.length - 1]; + const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1]; + + // Remove the typed tokenName + if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') { + // Remove spaces after the colon + if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') { + input.value = input.value.trim(); + } + + input.value = input.value.slice(0, -1 * lastSearchToken.length); + } else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) { + // Remove the existing tokenValue + const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`; + input.value = input.value.slice(0, -1 * lastTokenString.length); + } + + input.value += word; + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + + const filterIconPadding = 27; + const offset = gl.text + .getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; + let forceShowList = false; + + if (!mappingKey.reference) { + const dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + + // Passing glArguments to `new gl[glClass](<arguments>)` + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); + } + + if (firstLoad) { + mappingKey.reference.init(); + } + + if (this.currentDropdown === 'hint') { + // Force the dropdown to show if it was clicked from the hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + mappingKey.reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if (!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && this.mapping[match.key]; + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.key ? match.key : 'hint'; + this.load(key, firstLoad); + } + } + + setDropdown() { + const { lastToken, searchToken } = this.tokenizer + .processTokens(this.filteredSearchInput.value); + + if (this.filteredSearchInput.value.split('').last() === ' ') { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { + // Token is not fully initialized yet because it has no value + // Eg. token = 'label:' + + const split = lastToken.split(':'); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); + } else if (lastToken) { + // Token has been initialized into an object because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..ffd0d7e9cba086f9f4ea58840b7321a4182aface --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -0,0 +1,171 @@ +/* global Turbolinks */ + +(() => { + class FilteredSearchManager { + constructor() { + this.filteredSearchInput = document.querySelector('.filtered-search'); + this.clearSearchButton = document.querySelector('.clear-search'); + + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(); + + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + } + + cleanup() { + this.unbindEvents(); + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + bindEvents() { + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + } + + unbindEvents() { + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + } + + checkForBackspace(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } + } + + checkForEnter(e) { + if (e.keyCode === 13) { + e.preventDefault(); + + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + + this.search(); + } + } + + toggleClearSearchButton(e) { + if (e.target.value) { + this.clearSearchButton.classList.remove('hidden'); + } else { + this.clearSearchButton.classList.add('hidden'); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); + + this.dropdownManager.resetDropdowns(); + } + + loadSearchParamsFromURL() { + const params = gl.utils.getUrlParamsArray(); + const inputValues = []; + + params.forEach((p) => { + const split = p.split('='); + const keyParam = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); + + if (condition) { + inputValues.push(`${condition.tokenKey}:${condition.value}`); + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); + + if (match) { + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; + const symbol = match.symbol; + let quotationsToUse = ''; + + if (sanitizedValue.indexOf(' ') !== -1) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'search') { + inputValues.push(sanitizedValue); + } + } + }); + + // Trim the last space value + this.filteredSearchInput.value = inputValues.join(' '); + + if (inputValues.length > 0) { + this.clearSearchButton.classList.remove('hidden'); + } + } + + search() { + const paths = []; + const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const currentState = gl.utils.getParameterByName('state') || 'opened'; + paths.push(`state=${currentState}`); + + tokens.forEach((token) => { + const condition = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const keyParam = param ? `${token.key}_${param}` : token.key; + let tokenPath = ''; + + if (condition) { + tokenPath = condition.url; + } else { + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; + } + + paths.push(tokenPath); + }); + + if (searchToken) { + paths.push(`search=${encodeURIComponent(searchToken)}`); + } + + Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..e46373024b6223cc3bd0160379e969c558677aa0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -0,0 +1,83 @@ +(() => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + + class FilteredSearchTokenKeys { + static get() { + return tokenKeys; + } + + static getConditions() { + return conditions; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeys.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..cf53845a48bac7dac1b414245e3c64640b8ebe17 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -0,0 +1,45 @@ +(() => { + class FilteredSearchTokenizer { + static processTokens(input) { + // Regex extracts `(token):(symbol)(value)` + // Values that start with a double quote must end in a double quote (same for single) + const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; + const tokens = []; + let lastToken = null; + const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; + + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; + } + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + return ''; + }).replace(/\s{2,}/g, ' ').trim() || ''; + + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; + } + + return { + tokens, + lastToken, + searchToken, + }; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 2f3da7451194cf472a89632fd3c34fa24271e6f8..6ca543c2b0038e398d12d7bcff54f05ba91a6e8f 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,19 +1,29 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */ + // Creates the variables for setting up GFM auto-completion (function() { - if (window.GitLab == null) { - window.GitLab = {}; + if (window.gl == null) { + window.gl = {}; } function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); } - GitLab.GfmAutoComplete = { - dataLoading: false, - dataLoaded: false, + window.gl.GfmAutoComplete = { + dataSources: {}, + defaultLoadingData: ['loading'], cachedData: {}, - dataSource: '', + isLoadingData: {}, + atTypeMap: { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands' + }, // Emoji Emoji: { template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' @@ -34,46 +44,45 @@ template: '<li>${title}</li>' }, Loading: { - template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>' }, DefaultOptions: { sorter: function(query, items, searchKey) { - // Highlight first item only if at least one char was typed - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if ((items[0].name != null) && items[0].name === 'loading') { + this.setting.highlightFirst = query.length > 0; + if (gl.GfmAutoComplete.isLoading(items)) { return items; } return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); }, filter: function(query, data, searchKey) { - if (data[0] === 'loading') { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); return data; + } else { + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); } - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); }, beforeInsert: function(value) { if (value && !this.setting.skipSpecialCharacterTest) { var withoutAt = value.substring(1); if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; } - if (!GitLab.GfmAutoComplete.dataLoaded) { - return this.at; - } else { - return value; - } + return value; }, matcher: function (flag, subtext) { // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - var _a, _y, regexp, match; - subtext = subtext.split(' ').pop(); + var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + subtext = subtext.split(/\s+/g).pop(); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); match = regexp.exec(subtext); @@ -84,69 +93,44 @@ } } }, - setup: _.debounce(function(input) { + setup: function(input) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); - // destroy previous instances - this.destroyAtWho(); - // set up instances - this.setupAtWho(); - - if (this.dataSource && !this.dataLoading && !this.cachedData) { - this.dataLoading = true; - return this.fetchData(this.dataSource) - .done((data) => { - this.dataLoading = false; - this.loadData(data); - }); - }; - - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } - }, 1000), - setupAtWho: function() { + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + }); + }, + setupAtWho: function($input) { // Emoji - this.input.atwho({ + $input.atwho({ at: ':', - displayTpl: (function(_this) { - return function(value) { - if (value.path != null) { - return _this.Emoji.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: function(value) { + return value.path != null ? this.Emoji.template : this.Loading.template; + }.bind(this), insertTpl: ':${name}:', - data: ['loading'], - startWithSpace: false, skipSpecialCharacterTest: true, + data: this.defaultLoadingData, callbacks: { sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher + filter: this.DefaultOptions.filter } }); // Team Members - this.input.atwho({ + $input.atwho({ at: '@', - displayTpl: (function(_this) { - return function(value) { - if (value.username != null) { - return _this.Members.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), insertTpl: '${atwho-at}${username}', searchKey: 'search', - data: ['loading'], - startWithSpace: false, alwaysHighlightFirst: true, skipSpecialCharacterTest: true, + data: this.defaultLoadingData, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -177,22 +161,15 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '#', alias: 'issues', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), - data: ['loading'], + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', - startWithSpace: false, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -212,26 +189,20 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '%', alias: 'milestones', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Milestones.template; - } else { - return _this.Loading.template; - } - }; - })(this), insertTpl: '${atwho-at}${title}', - data: ['loading'], - startWithSpace: false, + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -246,21 +217,14 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '!', alias: 'mergerequests', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), - data: ['loading'], - startWithSpace: false, + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', callbacks: { sorter: this.DefaultOptions.sorter, @@ -281,18 +245,30 @@ } } }); - this.input.atwho({ + $input.atwho({ at: '~', alias: 'labels', searchKey: 'search', - displayTpl: this.Labels.template, + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), insertTpl: '${atwho-at}${title}', - startWithSpace: false, callbacks: { matcher: this.DefaultOptions.matcher, - sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, beforeSave: function(merges) { + if (gl.GfmAutoComplete.isLoading(merges)) return merges; + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; return $.map(merges, function(m) { return { title: sanitize(m.title), @@ -304,12 +280,14 @@ } }); // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - this.input.filter('[data-supports-slash-commands="true"]').atwho({ + $input.filter('[data-supports-slash-commands="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', skipSpecialCharacterTest: true, + data: this.defaultLoadingData, displayTpl: function(value) { + if (this.isLoading(value)) return this.Loading.template; var tpl = '<li>/${name}'; if (value.aliases.length > 0) { tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; @@ -322,7 +300,7 @@ } tpl += '</li>'; return _.template(tpl)(value); - }, + }.bind(this), insertTpl: function(value) { var tpl = "/${name} "; var reference_prefix = null; @@ -340,6 +318,7 @@ filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(commands) { + if (gl.GfmAutoComplete.isLoading(commands)) return commands; return $.map(commands, function(c) { var search = c.name; if (c.aliases.length > 0) { @@ -367,32 +346,29 @@ }); return; }, - destroyAtWho: function() { - return this.input.atwho('destroy'); - }, - fetchData: function(dataSource) { - return $.getJSON(dataSource); + fetchData: function($input, at) { + if (this.isLoadingData[at]) return; + this.isLoadingData[at] = true; + if (this.cachedData[at]) { + this.loadData($input, at, this.cachedData[at]); + } else { + $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + this.loadData($input, at, data); + }).fail(() => { this.isLoadingData[at] = false; }); + } }, - loadData: function(data) { - this.cachedData = data; - this.dataLoaded = true; - // load members - this.input.atwho('load', '@', data.members); - // load issues - this.input.atwho('load', 'issues', data.issues); - // load milestones - this.input.atwho('load', 'milestones', data.milestones); - // load merge requests - this.input.atwho('load', 'mergerequests', data.mergerequests); - // load emojis - this.input.atwho('load', ':', data.emojis); - // load labels - this.input.atwho('load', '~', data.labels); - // load commands - this.input.atwho('load', '/', data.commands); + loadData: function($input, at, data) { + this.isLoadingData[at] = false; + this.cachedData[at] = data; + $input.atwho('load', at, data); // This trigger at.js again // otherwise we would be stuck with loading until the user types - return $(':focus').trigger('keyup'); + return $input.trigger('keyup'); + }, + isLoading(data) { + if (!data || !data.length) return false; + if (Array.isArray(data)) data = data[0]; + return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; } }; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9a91018a8e4647bc9ad2fa1c04c3112ff1b67bb1..00859728c3084401c1055cb0c0f35214e5d2c276 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, no-undef, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */ +/* global fuzzaldrinPlus */ +/* global Turbolinks */ + (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, @@ -20,7 +23,6 @@ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - this.indeterminateIds = []; $clearButton.on('click', (function(_this) { // Clear click return function(e) { @@ -188,7 +190,7 @@ })(); GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex; + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; LOADING_CLASS = "is-loading"; @@ -341,16 +343,18 @@ selector = ".dropdown-page-one .dropdown-content a"; } this.dropdown.on("click", selector, function(e) { - var $el, selected; + var $el, selected, selectedObj, isMarking; $el = $(this); selected = self.rowClicked($el); + selectedObj = selected ? selected[0] : null; + isMarking = selected ? selected[1] : null; if (self.options.clicked) { - self.options.clicked(selected, $el, e); + self.options.clicked(selectedObj, $el, e, isMarking); } // Update label right after all modifications in dropdown has been done if (self.options.toggleLabel) { - self.updateLabel(selected, $el, self); + self.updateLabel(selectedObj, $el, self); } $el.trigger('blur'); @@ -441,12 +445,6 @@ this.resetRows(); this.addArrowKeyEvent(); - if (this.options.setIndeterminateIds) { - this.options.setIndeterminateIds.call(this); - } - if (this.options.setActiveIds) { - this.options.setActiveIds.call(this); - } // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); @@ -478,12 +476,7 @@ this.removeArrayKeyEvent(); $input = this.dropdown.find(".dropdown-input-field"); if (this.options.filterable) { - $input.blur().val(""); - } - // Triggering 'keyup' will re-render the dropdown which is not always required - // specially if we want to keep the state of the dropdown needed for bulk-assignment - if (!this.options.persistWhenHide) { - $input.trigger("input"); + $input.blur(); } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); @@ -617,7 +610,8 @@ }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; + fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { @@ -638,7 +632,7 @@ el.addClass(ACTIVE_CLASS); } - return selectedObject; + return [selectedObject]; } field = []; @@ -656,6 +650,7 @@ } if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { if (isInput) { @@ -665,6 +660,7 @@ } } } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); if (field && field.length && value == null) { @@ -674,6 +670,7 @@ this.addInput(fieldName, value, selectedObject); } } else { + isMarking = true; if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); if (!isInput) { @@ -694,7 +691,7 @@ } } - return selectedObject; + return [selectedObject, isMarking]; }; GitLabDropdown.prototype.focusTextInput = function() { diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 6ce392d2a5b54e117f87980da9025971d60ea46e..63f9cafa8d05cee108b59cdc1d87d360393b6768 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */ //= require gl_field_error diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index db5d9e75b3ab69f766b904502ed8f43e68ffc6c1..04814fa0843cee9cad8d5549cf9b8cc246572709 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-undef, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + (function() { this.GLForm = (function() { function GLForm(form) { @@ -26,13 +30,13 @@ this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); // form and textarea event listeners this.addEventListeners(); - gl.text.init(this.form); } + gl.text.init(this.form); // hide discard button this.form.find('.js-note-discard').hide(); return this.form.show(); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index c3a132b3c7561e3a453c280a42964bf6345a438e..2d08a7c6ac3c2b3aa945bec30b93d1fb617a9512 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,4 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, no-undef, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */ +/* global ContributorsGraph */ +/* global ContributorsAuthorGraph */ +/* global ContributorsMasterGraph */ +/* global ContributorsStatGraphUtil */ +/* global d3 */ /*= require d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index cb2448e8cc7b326bcd39969091f82dec466d1427..9c5e9381e5221335dd645a8099f326418dded0e3 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, no-undef, newline-per-chained-call, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */ +/* global d3 */ +/* global ContributorsGraph */ /*= require d3 */ diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 index eea6cd40859d897a5fe1d6356150cae460537356..8e10e4244126c004e7d762fef8643e12c07e95f5 100644 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ b/app/assets/javascripts/group_label_subscription.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */ + (function(global) { class GroupLabelSubscription { constructor(container) { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 3dc6f05ca2050500ef86b12bfece1b7b205c275a..99700e7562ade758bf7af868bca75423cf697ce7 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* global Api */ + (function() { var slice = [].slice; @@ -14,7 +16,7 @@ multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - options = { all_available: all_available, skip_groups: skip_groups }; + var options = { all_available: all_available, skip_groups: skip_groups }; return Api.groups(query.term, options, function(groups) { var data; data = { diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 9425b6ed9d4a2d6c13e344e2662424ed3f12bdd9..fa795be07ed95b5d1c37f5fc923e92dab1c13e1a 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */ + (function() { - this.ImporterStatus = (function() { + window.ImporterStatus = (function() { function ImporterStatus(jobs_url, import_url) { this.jobs_url = jobs_url; this.import_url = import_url; @@ -75,7 +76,7 @@ var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); var importPath = $('.js-importer-status').data('import-path'); - new ImporterStatus(jobsImportPath, importPath); + new window.ImporterStatus(jobsImportPath, importPath); } }); }).call(this); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 46503c290aed9ca9b6e9843602905a181f5a4f63..9c3c96c20ed96126c97edd3dc4cd13548c5bd38b 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,10 +1,13 @@ -/* eslint-disable */ -(function() { +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* global Issuable */ +/* global Turbolinks */ + +((global) => { var issuable_created; issuable_created = false; - this.Issuable = { + global.Issuable = { init: function() { Issuable.initTemplates(); Issuable.initSearch(); @@ -108,7 +111,11 @@ filterResults: (function(_this) { return function(form) { var formAction, formData, issuesUrl; - formData = form.serialize(); + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); @@ -141,6 +148,9 @@ const $issuesOtherFilters = $('.issues-other-filters'); const $issuesBulkUpdate = $('.issues_bulk_update'); + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + if ($checkedIssues.length > 0) { let ids = $.map($checkedIssues, function(value) { return $(value).data('id'); @@ -152,7 +162,6 @@ $updateIssuesIds.val([]); $issuesBulkUpdate.hide(); $issuesOtherFilters.show(); - this.issuableBulkActions.willUpdateLabels = false; } return true; }, @@ -179,4 +188,4 @@ } }; -}).call(this); +})(window); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..7d0465aa8b47d06b5d1eb68049f71d055a07e066 --- /dev/null +++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6 @@ -0,0 +1 @@ +//= require ./time_tracking/time_tracking_bundle diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..72433df2818d112cdb75ce402b853e68a263153f --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 @@ -0,0 +1,42 @@ +/* global Vue */ +//= require lib/utils/pretty_time + +(() => { + Vue.component('time-tracking-collapsed-state', { + name: 'time-tracking-collapsed-state', + props: [ + 'showComparisonState', + 'showSpentOnlyState', + 'showEstimateOnlyState', + 'showNoTimeTrackingState', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + 'stopwatchSvg', + ], + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + <div class='sidebar-collapsed-icon'> + <div v-html='stopwatchSvg'></div> + <div class='time-tracking-collapsed-summary'> + <div class='compare' v-if='showComparisonState'> + <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='estimate-only' v-if='showEstimateOnlyState'> + <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='spend-only' v-if='showSpentOnlyState'> + <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span> + </div> + <div class='no-tracking' v-if='showNoTimeTrackingState'> + <span class='no-value'>None</span> + </div> + </div> + </div> + `, + }); +})(); + diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..6abbd5dd16785e26acf91eed56202ae7032d9b35 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 @@ -0,0 +1,69 @@ +/* global Vue */ +//= require lib/utils/pretty_time + +(() => { + const prettyTime = gl.utils.prettyTime; + + Vue.component('time-tracking-comparison-pane', { + name: 'time-tracking-comparison-pane', + props: [ + 'timeSpent', + 'timeEstimate', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + computed: { + parsedRemaining() { + const diffSeconds = this.timeEstimate - this.timeSpent; + return prettyTime.parseSeconds(diffSeconds); + }, + timeRemainingHumanReadable() { + return prettyTime.stringifyTime(this.parsedRemaining); + }, + timeRemainingTooltip() { + const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; + return `${prefix} ${this.timeRemainingHumanReadable}`; + }, + /* Diff values for comparison meter */ + timeRemainingMinutes() { + return this.timeEstimate - this.timeSpent; + }, + timeRemainingPercent() { + return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + }, + timeRemainingStatusClass() { + return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; + }, + /* Parsed time values */ + parsedEstimate() { + return prettyTime.parseSeconds(this.timeEstimate); + }, + parsedSpent() { + return prettyTime.parseSeconds(this.timeSpent); + }, + }, + template: ` + <div class='time-tracking-comparison-pane'> + <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay' + :aria-valuenow='timeRemainingTooltip' + :title='timeRemainingTooltip' + :data-original-title='timeRemainingTooltip' + :class='timeRemainingStatusClass'> + <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'> + <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div> + </div> + <div class='compare-display-container'> + <div class='compare-display pull-left'> + <span class='compare-label'>Spent</span> + <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span> + </div> + <div class='compare-display estimated pull-right'> + <span class='compare-label'>Est</span> + <span class='compare-value'>{{ timeEstimateHumanReadable }}</span> + </div> + </div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..309e9f2f9ef65ed15bff24f85c1c389c57e691bb --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-estimate-only-pane', { + name: 'time-tracking-estimate-only-pane', + props: ['timeEstimateHumanReadable'], + template: ` + <div class='time-tracking-estimate-only-pane'> + <span class='bold'>Estimated:</span> + {{ timeEstimateHumanReadable }} + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..d7ced6d7151398a913bef2ed42d330a96db7671a --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 @@ -0,0 +1,24 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-help-state', { + name: 'time-tracking-help-state', + props: ['docsUrl'], + template: ` + <div class='time-tracking-help-state'> + <div class='time-tracking-info'> + <h4>Track time with slash commands</h4> + <p>Slash commands can be used in the issues description and comment boxes.</p> + <p> + <code>/estimate</code> + will update the estimated time with the latest command. + </p> + <p> + <code>/spend</code> + will update the sum of the time spent. + </p> + <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..1d2ca643b5b28cee6bfa964b5609881f2bdaf3b0 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 @@ -0,0 +1,11 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-no-tracking-pane', { + name: 'time-tracking-no-tracking-pane', + template: ` + <div class='time-tracking-no-tracking-pane'> + <span class='no-value'>No estimate or time spent</span> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..ed283fec3c353f1c01339e716c80b92491f3a111 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-spent-only-pane', { + name: 'time-tracking-spent-only-pane', + props: ['timeSpentHumanReadable'], + template: ` + <div class='time-tracking-spend-only-pane'> + <span class='bold'>Spent:</span> + {{ timeSpentHumanReadable }} + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..26563a7713bf8350415526d7987106ab02229539 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 @@ -0,0 +1,118 @@ +/* global Vue */ +//= require ./help_state +//= require ./collapsed_state +//= require ./spent_only_pane +//= require ./no_tracking_pane +//= require ./estimate_only_pane +//= require ./comparison_pane + +(() => { + Vue.component('issuable-time-tracker', { + name: 'issuable-time-tracker', + props: [ + 'time_estimate', + 'time_spent', + 'human_time_estimate', + 'human_time_spent', + 'stopwatchSvg', + 'docsUrl', + ], + data() { + return { + showHelp: false, + }; + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` + <div class='time_tracker time-tracking-component-wrap' v-cloak> + <time-tracking-collapsed-state + :show-comparison-state='showComparisonState' + :show-help-state='showHelpState' + :show-spent-only-state='showSpentOnlyState' + :show-estimate-only-state='showEstimateOnlyState' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable' + :stopwatch-svg='stopwatchSvg'> + </time-tracking-collapsed-state> + <div class='title hide-collapsed'> + Time tracking + <div class='help-button pull-right' + v-if='!showHelpState' + @click='toggleHelpState(true)'> + <i class='fa fa-question-circle'></i> + </div> + <div class='close-help-button pull-right' + v-if='showHelpState' + @click='toggleHelpState(false)'> + <i class='fa fa-close'></i> + </div> + </div> + <div class='time-tracking-content hide-collapsed'> + <time-tracking-estimate-only-pane + v-if='showEstimateOnlyState' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-estimate-only-pane> + <time-tracking-spent-only-pane + v-if='showSpentOnlyState' + :time-spent-human-readable='timeSpentHumanReadable'> + </time-tracking-spent-only-pane> + <time-tracking-no-tracking-pane + v-if='showNoTimeTrackingState'> + </time-tracking-no-tracking-pane> + <time-tracking-comparison-pane + v-if='showComparisonState' + :time-estimate='timeEstimate' + :time-spent='timeSpent' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-comparison-pane> + <transition name='help-state-toggle'> + <time-tracking-help-state + v-if='showHelpState' + :docs-url='docsUrl'> + </time-tracking-help-state> + </transition> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..0b8da2b1f4f7991c7545b5775f55dc7c8fd37340 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 @@ -0,0 +1,61 @@ +/* global Vue */ +//= require ./components/time_tracker +//= require smart_interval +//= require subbable_resource + +(() => { + /* This Vue instance represents what will become the parent instance for the + * sidebar. It will be responsible for managing `issuable` state and propagating + * changes to sidebar components. We will want to create a separate service to + * interface with the server at that point. + */ + + class IssuableTimeTracking { + constructor(issuableJSON) { + const parsedIssuable = JSON.parse(issuableJSON); + return this.initComponent(parsedIssuable); + } + + initComponent(parsedIssuable) { + this.parentInstance = new Vue({ + el: '#issuable-time-tracker', + data: { + issuable: parsedIssuable, + }, + methods: { + fetchIssuable() { + return gl.IssuableResource.get.call(gl.IssuableResource, { + type: 'GET', + url: gl.IssuableResource.endpoint, + }); + }, + updateState(data) { + this.issuable = data; + }, + subscribeToUpdates() { + gl.IssuableResource.subscribe(data => this.updateState(data)); + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes; + + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + this.fetchIssuable(); + }, + mounted() { + this.subscribeToUpdates(); + this.listenForSlashCommands(); + }, + }); + } + } + + gl.IssuableTimeTracking = IssuableTimeTracking; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 317818951fd5053caa8d25378127e709250a9eb0..4aaad111082f516ecded68a7b0ac0e80ac24306d 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */ +/* global UsersSelect */ + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 50fdbc89c7c5cb66ade9f371e5500c19b5122bc9..1c4086517fea934c7d33f68595b09d1ae5a0c158 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,4 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-undef, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* global GitLab */ +/* global UsersSelect */ +/* global ZenMode */ +/* global Autosave */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -14,7 +19,7 @@ this.renderWipExplanation = bind(this.renderWipExplanation, this); this.resetAutosave = bind(this.resetAutosave, this); this.handleSubmit = bind(this.handleSubmit, this); - GitLab.GfmAutoComplete.setup(); + gl.GfmAutoComplete.setup(); new UsersSelect(); new ZenMode(); this.titleField = this.form.find("input[name*='[title]']"); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 8540b199abaca7a682e1d83851cd55667e4d0259..61e8531153b728810d1d61e1c9b22b8fc879d533 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-undef, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* global Flash */ /*= require flash */ /*= require jquery.waitforimages */ @@ -138,15 +139,12 @@ return; } return $.getJSON($container.data('path')).error(function() { - $container.find('.checking').hide(); $container.find('.unavailable').show(); return new Flash('Failed to check if a new branch can be created.', 'alert'); }).success(function(data) { if (data.can_create_branch) { - $container.find('.checking').hide(); $container.find('.available').show(); } else { - $container.find('.checking').hide(); return $container.find('.unavailable').show(); } }); diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 9697fb33566b3a534c9950a79a8764e4811287cc..52fd5d71b188b6c3a4b28f7f818cc39825d6a824 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,10 +1,14 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ +/* global Issuable */ +/* global Flash */ + ((global) => { class IssuableBulkActions { - constructor({ container, form, issues } = {}) { - this.container = container || $('.content'), + constructor({ container, form, issues, prefixId } = {}) { + this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); + this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; @@ -13,10 +17,6 @@ Issuable.initChecks(); } - getElement(selector) { - return this.container.find(selector); - } - bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); } @@ -70,10 +70,7 @@ getUnmarkedIndeterminedLabels() { const result = []; - const labelsToKeep = []; - - this.getElement('.labels-filter .is-indeterminate') - .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + const labelsToKeep = this.$labelDropdown.data('indeterminate'); this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { @@ -103,45 +100,65 @@ } }; if (this.willUpdateLabels) { - this.getLabelsToApply().map(function(id) { - return formData.update.add_label_ids.push(id); - }); - this.getLabelsToRemove().map(function(id) { - return formData.update.remove_label_ids.push(id); - }); + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); } return formData; } - getLabelsToApply() { + setOriginalDropdownData() { + const $labelSelect = $('.bulk-update .js-label-select'); + $labelSelect.data('common', this.getOriginalCommonIds()); + $labelSelect.data('marked', this.getOriginalMarkedIds()); + $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); + } + + // From issuable's initial bulk selection + getOriginalCommonIds() { const labelIds = []; - const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); - $labels.each(function(k, label) { - if (label) { - return labelIds.push(parseInt($(label).val())); - } + + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); - return labelIds; + return _.intersection.apply(this, labelIds); } + // From issuable's initial bulk selection + getOriginalMarkedIds() { + const labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } - /** - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - */ - - getLabelsToRemove() { - const result = []; - const indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - const labelsToApply = this.getLabelsToApply(); - indeterminatedLabels.map(function(id) { - // We need to exclude label IDs that will be applied - // By not doing this will cause issues from selection to not add labels at all - if (labelsToApply.indexOf(id) === -1) { - return result.push(id); - } + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + const uniqueIds = []; + const labelIds = []; + let issuableLabels = []; + + // Collect unique label IDs for all checked issues + this.getElement('.selected_issue:checked').each((i, el) => { + issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); + issuableLabels.forEach((labelId) => { + // Store unique IDs + if (uniqueIds.indexOf(labelId) === -1) { + uniqueIds.push(labelId); + } + }); + // Store array of IDs per issuable + labelIds.push(issuableLabels); }); - return result; + // Add uniqueIds to add it as argument for _.intersection + labelIds.unshift(uniqueIds); + // Return IDs that are present but not in all selected issueables + return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); + } + + getElement(selector) { + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); } } diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index 175623e74483b0c1d4e710e4fba6642254575b1a..33c5e35324d856ea3b449cd619b34b32863c7029 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */ +/* global Flash */ + ((global) => { class LabelManager { @@ -104,4 +106,3 @@ gl.LabelManager = LabelManager; })(window.gl || (window.gl = {})); - diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f334f35594da739e2fc476fd1d931b6c6657eb4e..ec2fc87bece939f9509e45a4162f00ee6da69a86 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,12 +1,16 @@ -/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ +/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */ +/* global Issuable */ +/* global ListLabel */ + (function() { this.LabelsSelect = (function() { function LabelsSelect() { var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); + $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); @@ -122,7 +126,7 @@ }); }); }; - return $dropdown.glDropdown({ + $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ @@ -169,33 +173,40 @@ }); }, renderRow: function(label, instance) { - var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { - indeterminate = instance.indeterminateIds; - active = instance.activeIds; + indeterminate = $dropdown.data('indeterminate') || []; + marked = $dropdown.data('marked') || []; + if (indeterminate.indexOf(label.id) !== -1) { selectedClass.push('is-indeterminate'); } - if (active.indexOf(label.id) !== -1) { + + if (marked.indexOf(label.id) !== -1) { // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); - // Add input manually - instance.addInput(this.fieldName, label.id); } - } - if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { - selectedClass.push('is-active'); - } - if ($dropdown.hasClass('js-multiselect') && removesAll) { - selectedClass.push('dropdown-clear-active'); + } else { + if (this.id(label)) { + dropdownName = $dropdown.data('fieldName'); + dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); + + if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + selectedClass.push('is-active'); + } + } + + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } } if (label.duplicate) { spacing = 100 / label.color.length; @@ -231,7 +242,6 @@ // Return generated html return $li.html($a).prop('outerHTML'); }, - persistWhenHide: $dropdown.data('persistWhenHide'), search: { fields: ['title'] }, @@ -310,18 +320,15 @@ } } } - if ($dropdown.hasClass('js-filter-bulk-update')) { - // If we are persisting state we need the classes - if (!this.options.persistWhenHide) { - return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); - } - } }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(label, $el, e) { + clicked: function(label, $el, e, isMarking) { var isIssueIndex, isMRIndex, page; - _this.enableBulkLabelDropdown(); + + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { $dropdown.parent() @@ -330,12 +337,11 @@ } if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + _this.enableBulkLabelDropdown(); + _this.setDropdownData($dropdown, isMarking, this.id(label)); return; } - page = $('body').data('page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = page === 'projects:merge_requests:index'; if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; @@ -397,17 +403,10 @@ } } }, - setIndeterminateIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.indeterminateIds = _this.getIndeterminateIds(); - } - }, - setActiveIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.activeIds = _this.getActiveIds(); - } - } }); + + // Set dropdown data + _this.setOriginalDropdownData($dropdownContainer, $dropdown); }); this.bindEvents(); } @@ -420,34 +419,9 @@ if ($('.selected_issue:checked').length) { return; } - // Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); - // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; - LabelsSelect.prototype.getIndeterminateIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.flatten(label_ids); - }; - - LabelsSelect.prototype.getActiveIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.intersection.apply(_, label_ids); - }; - LabelsSelect.prototype.enableBulkLabelDropdown = function() { var issuableBulkActions; if ($('.selected_issue:checked').length) { @@ -456,8 +430,59 @@ } }; - return LabelsSelect; + LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { + var i, markedIds, unmarkedIds, indeterminateIds; + var issuableBulkActions = $('.bulk-update').data('bulkActions'); + markedIds = $dropdown.data('marked') || []; + unmarkedIds = $dropdown.data('unmarked') || []; + indeterminateIds = $dropdown.data('indeterminate') || []; + + if (isMarking) { + markedIds.push(value); + + i = indeterminateIds.indexOf(value); + if (i > -1) { + indeterminateIds.splice(i, 1); + } + + i = unmarkedIds.indexOf(value); + if (i > -1) { + unmarkedIds.splice(i, 1); + } + } else { + // If marked item (not common) is unmarked + i = markedIds.indexOf(value); + if (i > -1) { + markedIds.splice(i, 1); + } + + // If an indeterminate item is being unmarked + if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + + // If a marked item is being unmarked + // (a marked item could also be a label that is present in all selection) + if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + } + + $dropdown.data('marked', markedIds); + $dropdown.data('unmarked', unmarkedIds); + $dropdown.data('indeterminate', indeterminateIds); + }; + + LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { + var labels = []; + $container.find('[name="label_name[]"]').map(function() { + return labels.push(this.value); + }); + $dropdown.data('marked', labels); + }; + + return LabelsSelect; })(); }).call(this); diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index b1718e89d3deeb018467fed3547fd566b0d3bca2..4cdf99cae72578e74b28e564563a5db555039fa8 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,3 +1,2 @@ -/* eslint-disable */ /*= require ace-rails-ap */ /*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js.es6 similarity index 67% rename from app/assets/javascripts/lib/utils/common_utils.js rename to app/assets/javascripts/lib/utils/common_utils.js.es6 index 8fa80502d922df524805d5bf4445ce80e3b33af4..0c6a3cc31704dc98a3bc4163e8e2bdea582a91be 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -93,8 +93,22 @@ } }; - gl.utils.getPagePath = function() { - return $('body').data('page').split(':')[0]; + // Check if element scrolled into viewport from above or below + // Courtesy http://stackoverflow.com/a/7557433/414749 + w.gl.utils.isInViewport = function(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); + }; + + gl.utils.getPagePath = function(index) { + index = index || 0; + return $('body').data('page').split(':')[index]; }; gl.utils.parseUrl = function (url) { @@ -110,10 +124,42 @@ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; }; + gl.utils.getUrlParamsArray = function () { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + return window.location.search.slice(1).split('&'); + }; + gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; + gl.utils.scrollToElement = function($el) { + var top = $el.offset().top; + gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); + gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height(); + gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); + + return $('body, html').animate({ + scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight) + }, 200); + }; + + /** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided + */ + w.gl.utils.getParameterByName = (name) => { + const url = window.location.href; + name = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + })(window); }).call(this); diff --git a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 deleted file mode 100644 index 5ae978010c9c0379e5d95f21e8b990cd4ba9a458..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/** - * CustomEvent support for IE - */ -if (typeof window.CustomEvent !== 'function') { - window.CustomEvent = function CustomEvent(e, params) { - const options = params || { bubbles: false, cancelable: false, detail: undefined }; - const evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail); - return evt; - }; - window.CustomEvent.prototype = window.Event.prototype; -} diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index d0fe69260a53496bd7ec58c7a03d357a6a5eead9..3c9ad0e67c85e8c491418cf6e2fce21a0ea9f93b 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, no-undef, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ + (function() { (function(w) { var notificationGranted, notifyMe, notifyPermissions; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6 index ccaf447eb0b0651e761b4e7793e43acd7e2e2e12..ae397212e55f750deb701d35c184e3f2222c62f8 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js.es6 +++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6 @@ -4,13 +4,13 @@ * stringifyTime condensed or non-condensed, abbreviateTimelengths) * */ - class PrettyTime { - + const utils = window.gl.utils = gl.utils || {}; + const prettyTime = utils.prettyTime = { /* * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } * Seconds can be negative or positive, zero or non-zero. */ - static parseSeconds(seconds) { + parseSeconds(seconds) { const DAYS_PER_WEEK = 5; const HOURS_PER_DAY = 8; const MINUTES_PER_HOUR = 60; @@ -24,7 +24,7 @@ minutes: 1, }; - let unorderedMinutes = PrettyTime.secondsToMinutes(seconds); + let unorderedMinutes = prettyTime.secondsToMinutes(seconds); return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); @@ -33,35 +33,33 @@ return periodCount; }); - } + }, /* * Accepts a timeObject and returns a condensed string representation of it * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. */ - static stringifyTime(timeObject) { + stringifyTime(timeObject) { const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { const isNonZero = !!unitValue; return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; }, '').trim(); return reducedTime.length ? reducedTime : '0m'; - } + }, /* * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns * the first non-zero unit/value pair. */ - static abbreviateTime(timeStr) { + abbreviateTime(timeStr) { return timeStr.split(' ') .filter(unitStr => unitStr.charAt(0) !== '0')[0]; - } + }, - static secondsToMinutes(seconds) { + secondsToMinutes(seconds) { return Math.abs(seconds / 60); - } - } - - gl.PrettyTime = PrettyTime; + }, + }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index ac44b81ee22e33b7e693547456a99d8b48da8e70..c856a26ae402b91739336023e1b024ba31ac2f9c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -17,6 +17,21 @@ gl.text.replaceRange = function(s, start, end, substitute) { return s.substring(0, start) + substitute + s.substring(end); }; + gl.text.getTextWidth = function(text, font) { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + // re-use canvas object for better performance + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); + var context = canvas.getContext('2d'); + context.font = font; + return context.measureText(text).width; + }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); }; @@ -44,9 +59,25 @@ } }; gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar; + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine; + removedLastNewLine = false; + removedFirstNewLine = false; + + // Remove the first newline + if (selected.indexOf('\n') === 0) { + removedFirstNewLine = true; + selected = selected.replace(/\n+/, ''); + } + + // Remove the last newline + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + selectedSplit = selected.split('\n'); startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; + if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { if (blockTag != null) { insertText = this.blockTagText(text, textArea, blockTag, selected); @@ -62,6 +93,15 @@ } else { insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); } + + if (removedFirstNewLine) { + insertText = '\n' + insertText; + } + + if (removedLastNewLine) { + insertText += '\n'; + } + if (document.queryCommandSupported('insertText')) { inserted = document.execCommand('insertText', false, insertText); } @@ -74,9 +114,9 @@ document.execCommand("ms-endUndoUnit"); } catch (error) {} } - return this.moveCursor(textArea, tag, wrap); + return this.moveCursor(textArea, tag, wrap, removedLastNewLine); }; - gl.text.moveCursor = function(textArea, tag, wrapped) { + gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { var pos; if (!textArea.setSelectionRange) { return; @@ -87,6 +127,11 @@ } else { pos = textArea.selectionStart; } + + if (removedLastNewLine) { + pos -= 1; + } + return textArea.setSelectionRange(pos, pos); } }; diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..eff1dcabfa2d5b381b3c519711c9cc90f704f84a --- /dev/null +++ b/app/assets/javascripts/lib/vue_resource.js.es6 @@ -0,0 +1,2 @@ +//= require vue +//= require vue-resource diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index b0f834705c3f90ff266768bd5fc58f9aa805b435..9af89b79f84bd5f0f18bd382f5a3b1b2e2c3a2b2 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, no-undef, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */ + // LineHighlighter // // Handles single- and multi-line selection and highlight for blob views. diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js.es6 similarity index 66% rename from app/assets/javascripts/member_expiration_date.js rename to app/assets/javascripts/member_expiration_date.js.es6 index 7741cd2979334ea5402371b76e1a65d7c3ca3412..bf6c0ec27982d962f0ed6a28b61c98a00415d422 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js.es6 @@ -1,30 +1,29 @@ -/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */ -(function() { +(() => { // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling // `js-clear-input` element, then show that element when there is a value in the // datepicker, and make clicking on that element clear the field. // - gl.MemberExpirationDate = function() { + window.gl = window.gl || {}; + gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => { function toggleClearInput() { $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); } - - var inputs = $('.js-access-expiration-date'); + const inputs = $(selector); inputs.datepicker({ dateFormat: 'yy-mm-dd', minDate: 1, - onSelect: function () { + onSelect: function onSelect() { $(this).trigger('change'); toggleClearInput.call(this); - } + }, }); - inputs.next('.js-clear-input').on('click', function(event) { + inputs.next('.js-clear-input').on('click', function clicked(event) { event.preventDefault(); - var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); + const input = $(this).closest('.clearable-input').find(selector); input.datepicker('setDate', null) .trigger('change'); toggleClearInput.call(input); diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 index 9e4ffd07dbd64358d3d69c8223a350e98baba038..f95b079c972194861a90cef76faf6861034817ea 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */ +/* global Vue */ +/* global ace */ +/* global Flash */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 index 23c4618af7048852a8a83ef39634d445ba8bae1d..74544b7d0c73dbd09a982b7dbf2687fceab67044 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 index 4ccbdcd6daa2024bd7dfa44506fda28654475f5d..78c00c31c16b1c2cd7dbd0540f38ea73fa98f725 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -19,7 +21,7 @@ </td> <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td> <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td> - </template> + </template> </tr> </table> `, diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 index 8a7519b07869e540e1989568c6383b9a63b6aa36..8df3170edac9de93ca0d4901acdbbbe559707e32 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index f94e51e783cbfe88d6214e8f65006b642c39fc84..53b44007510c4cdf0540fa682aec02b9dcee1c4e 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */ +/* global Cookies */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 815443fb54e3b6c7c9fddc2826706fd2957c56c3..83520702f9b67e6ecf26e1469eed84474d6491b4 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable new-cap, comma-dangle, no-new, semi */ +/* global Vue */ +/* global Flash */ + //= require vue //= require ./merge_conflict_store //= require ./merge_conflict_service diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 index c8de586aa212720d182badd13ba3ff647f714038..e89b35d5407f4f04ef54b0f3c94664f027f679d1 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 index 88c3a20ce132b8ba2fa2c6c207b6d06fca51db81..a4aca85d46013060623f68390a2ca2c7c98246f8 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 88c3636be6cf84394355e4a40210ce7e7cec48b6..244c2f6746c9c6cc676e5cf16cb38b7eff1ea1c9 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */ +/* global MergeRequestTabs */ /*= require jquery.waitforimages */ /*= require task_list */ @@ -26,6 +27,7 @@ // Prevent duplicate event bindings this.disableTaskList(); this.initMRBtnListeners(); + this.initCommitMessageListeners(); if ($("a.btn-close").length) { this.initTaskList(); } @@ -107,6 +109,26 @@ // note so that we can re-use its form here }; + MergeRequest.prototype.initCommitMessageListeners = function() { + var textarea = $('textarea.js-commit-message'); + + $('a.js-with-description-link').on('click', function(e) { + e.preventDefault(); + + textarea.val(textarea.data('messageWithDescription')); + $('p.js-with-description-hint').hide(); + $('p.js-without-description-hint').show(); + }); + + $('a.js-without-description-link').on('click', function(e) { + e.preventDefault(); + + textarea.val(textarea.data('messageWithoutDescription')); + $('p.js-with-description-hint').show(); + $('p.js-without-description-hint').hide(); + }); + }; + return MergeRequest; })(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 3ec0f1fd613e553341e2820641621232f591dc23..4c8c28af7552815d948ee069e22f66e5987a3e4a 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -59,16 +59,13 @@ class MergeRequestTabs { - constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) { + constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; - this.buildsLoaded = false; this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; this.setUrl = setUrl !== undefined ? setUrl : true; - this.buildsLoaded = buildsLoaded || false; - this.setCurrentAction = this.setCurrentAction.bind(this); this.tabShown = this.tabShown.bind(this); this.showTab = this.showTab.bind(this); @@ -119,10 +116,6 @@ $.scrollTo('.merge-request-details .merge-request-tabs', { offset: -navBarHeight, }); - } else if (action === 'builds') { - this.loadBuilds($target.attr('href')); - this.expandView(); - this.resetViewContainer(); } else if (action === 'pipelines') { this.loadPipelines($target.attr('href')); this.expandView(); @@ -180,8 +173,8 @@ setCurrentAction(action) { this.currentAction = action === 'show' ? 'notes' : action; - // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' if (this.currentAction !== 'notes') { @@ -244,29 +237,8 @@ } this.diffsLoaded = true; - const diffPage = new gl.Diff(); - - const locationHash = gl.utils.getLocationHash(); - const anchoredDiff = locationHash && locationHash.split('_')[0]; - if (anchoredDiff) { - diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs')); - } - }, - }); - } - - loadBuilds(source) { - if (this.buildsLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - document.querySelector('div#builds').innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); - this.buildsLoaded = true; - new gl.Pipelines(); - this.scrollToElement('#builds'); + new gl.Diff(); + this.scrollToElement('#diffs'); }, }); } @@ -282,6 +254,10 @@ gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); this.pipelinesLoaded = true; this.scrollToElement('#pipelines'); + + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); }, }); } diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7022aa1263b8ee55b57c2dd12c754c137d9316c3..0305aeb07d9f59afd854b987eb2c64eb2dd5420d 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,5 +1,10 @@ -/* eslint-disable */ - ((global) => { +/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */ +/* global notify */ +/* global notifyPermissions */ +/* global merge_request_widget */ +/* global Turbolinks */ + +((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> @@ -27,7 +32,7 @@ </div> </div>`; - global.MergeRequestWidget = (function() { + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -69,7 +74,7 @@ MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; - allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; + allowedPages = ['show', 'commits', 'pipelines', 'changes']; $(document).on('page:change.merge_request', (function(_this) { return function() { var page; @@ -82,10 +87,10 @@ }; MergeRequestWidget.prototype.retrieveSuccessIcon = function() { - const $ciSuccessIcon = $('.js-success-icon'); - this.$ciSuccessIcon = $ciSuccessIcon.html(); - $ciSuccessIcon.remove(); - } + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + } MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { @@ -168,7 +173,6 @@ message = message.replace('{{title}}', data.title); notify(title, message, _this.opts.gitlab_icon, function() { this.close(); - return Turbolinks.visit(_this.opts.builds_path); }); } } @@ -204,7 +208,7 @@ const template = _.template(templateString)(environment) this.$widgetBody.before(template); } - }; + }; MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; @@ -246,4 +250,4 @@ })(); - })(window.gl || (window.gl = {})); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..2b074994b4ab9029f6e201d7e5981aa3f5556158 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -0,0 +1,42 @@ +/* global merge_request_widget */ + +(() => { + $(() => { + /* TODO: This needs a better home, or should be refactored. It was previously contained + * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, + * but Vue chokes on script tags and prevents their execution. So it was moved here + * temporarily. + * */ + + if ($('.accept-mr-form').length) { + $('.accept-mr-form').on('ajax:send', () => { + $('.accept-mr-form :input').disable(); + }); + + $('.accept_merge_request').on('click', () => { + $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); + }); + + $('.merge_when_build_succeeds').on('click', () => { + $('#merge_when_build_succeeds').val('1'); + }); + + $('.js-merge-dropdown a').on('click', (e) => { + e.preventDefault(); + $(this).closest('form').submit(); + }); + } else if ($('.rebase-in-progress').length) { + merge_request_widget.rebaseInProgress(); + } else if ($('.rebase-mr-form').length) { + $('.rebase-mr-form').on('ajax:send', () => { + $('.rebase-mr-form :input').disable(); + }); + + $('.js-rebase-button').on('click', () => { + $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); + }); + } else { + merge_request_widget.getMergeStatus(); + } + }); +})(); diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 15a12c3d98521276f4c27e2b6afc521217bfb32b..9f8af46c7158b0860a165dfb2a27753a2a646eac 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index db7561a3a7561b4532c017926982bbfb1d342d88..42152362e60ee63e58b6d81c3aa52947ce3d3fa3 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, no-undef, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* global Flash */ + (function() { this.Milestone = (function() { Milestone.updateIssue = function(li, issue_url, data) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 67796083790a8d13584a9a6fb5956015b268559c..28054b7824992874ae2e26a1af95cf2c1bee79e3 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-undef, no-param-reassign, no-shadow, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */ +/* global Vue */ +/* global Issuable */ +/* global ListMilestone */ + (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject) { diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..90b3366f14b83a223b48ee3cc02e4bc3aeee8979 --- /dev/null +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -0,0 +1,96 @@ +/* eslint-disable no-new */ +/* global Flash */ + +/** + * In each pipelines table we have a mini pipeline graph for each pipeline. + * + * When we click in a pipeline stage, we need to make an API call to get the + * builds list to render in a dropdown. + * + * The container should be the table element. + * + * The stage icon clicked needs to have the following HTML structure: + * <div> + * <button class="dropdown js-builds-dropdown-button"></button> + * <div class="js-builds-dropdown-container"></div> + * </div> + */ +(() => { + class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + + this.bindEvents(); + } + + /** + * Adds and removes the event listener. + */ + bindEvents() { + const dropdownButtonSelector = 'button.js-builds-dropdown-button'; + + $(this.container).off('click', dropdownButtonSelector, this.getBuildsList) + .on('click', dropdownButtonSelector, this.getBuildsList); + } + + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); + + dropdownContainer.innerHTML = data; + } + + /** + * For the clicked stage, gets the list of builds. + * + * @param {Object} e + * @return {Promise} + */ + getBuildsList(e) { + const button = e.currentTarget; + const endpoint = button.dataset.stageEndpoint; + + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); + } + + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } + } + + window.gl = window.gl || {}; + window.gl.MiniPipelineGraph = MiniPipelineGraph; +})(); diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 87c903ec5763c502c34d23f3f4ba0b90352b1dab..6633f2c27095134092d4a9d341585c204eba6404 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,8 +1,10 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, no-undef, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* global Api */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.NamespaceSelect = (function() { + window.NamespaceSelect = (function() { function NamespaceSelect(opts) { this.onSelectItem = bind(this.onSelectItem, this); var fieldName, showAny; @@ -64,7 +66,7 @@ })(); - this.NamespaceSelects = (function() { + window.NamespaceSelects = (function() { function NamespaceSelects(opts) { var ref; if (opts == null) { @@ -74,7 +76,7 @@ this.$dropdowns.each(function(i, dropdown) { var $dropdown; $dropdown = $(dropdown); - return new NamespaceSelect({ + return new window.NamespaceSelect({ dropdown: $dropdown }); }); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index e3dc599b90ab9d58e378c9075e35fe1bd5769928..20a68780cd5129f4cfad844d0e03beeac04d49a2 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-undef, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */ +/* global Raphael */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -354,7 +356,7 @@ icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); nameText = this.text(x + 25, y + 10, commit.author.name); idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message); + messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n ")); textSet = this.set(icon, nameText, idText, messageText).attr({ "text-anchor": "start", font: "12px Monaco, monospace" @@ -366,6 +368,7 @@ idText.attr({ fill: "#AAA" }); + messageText.node.style["white-space"] = "pre"; this.textWrap(messageText, boxWidth - 50); rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ fill: "#FFF", @@ -402,16 +405,21 @@ s.push("\n"); x = 0; } - x += word.length * letterWidth; - s.push(word + " "); + if (word === "\n") { + s.push("\n"); + x = 0; + } else { + s.push(word + " "); + x += word.length * letterWidth; + } } t.attr({ - text: s.join("") + text: s.join("").trim() }); b = t.getBBox(); - h = Math.abs(b.y2) - Math.abs(b.y) + 1; + h = Math.abs(b.y2) + 1; return t.attr({ - y: b.y + h + y: h }); }; diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 5a8f723a27b678f460084146bfb5182ddb4a60f0..2367d2497b2268202f18be22323fe32817280570 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, no-undef, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* global BranchGraph */ + (function() { this.Network = (function() { function Network(opts) { diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 732d92845cbf658d7823819c142192b8cbb61631..17833d3419a956bf1831c8cf11b2dacd25867114 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, no-undef, comma-dangle, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */ +/* global Network */ +/* global ShortcutsNetwork */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0ca0e2555951efe44b9c87c006a3f0cd98220de0..fac21f8cd32e14f114cb54894cd3662d365830be 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,9 @@ -/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ +/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */ +/* global Flash */ +/* global GLForm */ +/* global Autosave */ +/* global ResolveService */ +/* global mrRefreshWidgetUrl */ /*= require autosave */ /*= require autosize */ @@ -48,6 +53,12 @@ this.setupMainTargetNoteForm(); this.initTaskList(); this.collapseLongCommitList(); + + // We are in the Merge Requests page so we need another edit form for Changes tab + if (gl.utils.getPagePath(1) === 'merge_requests') { + $('.note-edit-form').clone() + .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + } } Notes.prototype.addBinding = function() { @@ -59,7 +70,7 @@ // change note in UI after update $(document).on("ajax:success", "form.edit-note", this.updateNote); // Edit note link - $(document).on("click", ".js-note-edit", this.showEditForm); + $(document).on("click", ".js-note-edit", this.showEditForm.bind(this)); $(document).on("click", ".note-edit-cancel", this.cancelEdit); // Reopen and close actions for Issue/MR combined with note form submit $(document).on("click", ".js-comment-button", this.updateCloseButton); @@ -234,6 +245,16 @@ }; + Notes.prototype.handleCreateChanges = function(note) { + if (typeof note === 'undefined') { + return; + } + + if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) { + $.get(mrRefreshWidgetUrl); + } + }; + /* Render note in main comments area. @@ -305,7 +326,7 @@ } row = form.closest("tr"); note_html = $(note.html); - note_html.syntaxHighlight(); + note_html.renderGFM(); // is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); if ((note.original_discussion_id != null) && discussionContainer.length === 0) { @@ -322,7 +343,7 @@ discussionContainer.append(note_html); // Init discussion on 'Discussion' page if it is merge request page if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { - $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); + $('ul.main-notes-list').append(note.discussion_html).renderGFM(); } } else { // append new note to all matching discussions @@ -419,6 +440,7 @@ */ Notes.prototype.addNote = function(xhr, note, status) { + this.handleCreateChanges(note); return this.renderNote(note); }; @@ -462,8 +484,9 @@ var $html, $note_li; // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); + this.revertNoteEditForm(); gl.utils.localTimeAgo($('.js-timeago', $html)); - $html.syntaxHighlight(); + $html.renderGFM(); $html.find('.js-task-list-container').taskList('enable'); // Find the note's `li` element by ID and replace it with the updated HTML $note_li = $('.note-row-' + note.id); @@ -476,48 +499,56 @@ }; + Notes.prototype.checkContentToAllowEditing = function($el) { + var initialContent = $el.find('.original-note-content').text().trim(); + var currentContent = $el.find('.note-textarea').val(); + var isAllowed = true; + + if (currentContent === initialContent) { + this.removeNoteEditForm($el); + } + else { + var $buttons = $el.find('.note-form-actions'); + var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + + if (!isWidgetVisible) { + gl.utils.scrollToElement($el); + } + + $el.find('.js-edit-warning').show(); + isAllowed = false; + } + + return isAllowed; + } + + /* Called in response to clicking the edit note link Replaces the note text with the note edit form Adds a data attribute to the form with the original content of the note for cancellations - */ - + */ Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { - var $noteText, done, form, note; e.preventDefault(); - note = $(this).closest(".note"); - note.addClass("is-editting"); - form = note.find(".note-edit-form"); - form.addClass('current-note-edit-form'); - // Show the attachment delete link - note.find(".js-note-attachment-delete").show(); - done = function($noteText) { - var noteTextVal; - // Neat little trick to put the cursor at the end - noteTextVal = $noteText.val(); - // Store the original note text in a data attribute to retrieve if a user cancels edit. - form.find('form.edit-note').data('original-note', noteTextVal); - return $noteText.val('').val(noteTextVal); - }; - new GLForm(form); - if ((scrollTo != null) && (myLastNote != null)) { - // scroll to the bottom - // so the open of the last element doesn't make a jump - $('html, body').scrollTop($(document).height()); - return $('html, body').animate({ - scrollTop: myLastNote.offset().top - 150 - }, 500, function() { - var $noteText; - $noteText = form.find(".js-note-text"); - $noteText.focus(); - return done($noteText); - }); - } else { - $noteText = form.find('.js-note-text'); - $noteText.focus(); - return done($noteText); + + var $target = $(e.target); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $target.closest('.note'); + var $currentlyEditing = $('.note.is-editting:visible'); + + if ($currentlyEditing.length) { + var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); + + if (!isEditAllowed) { + return; + } } + + $note.find('.js-note-attachment-delete').show(); + $editForm.addClass('current-note-edit-form'); + $note.addClass('is-editting'); + this.putEditFormInPlace($target); }; @@ -528,19 +559,41 @@ */ Notes.prototype.cancelEdit = function(e) { - var note; e.preventDefault(); - note = $(e.target).closest('.note'); + var $target = $(e.target); + var note = $target.closest('.note'); + note.find('.js-edit-warning').hide(); + this.revertNoteEditForm($target); return this.removeNoteEditForm(note); }; + Notes.prototype.revertNoteEditForm = function($target) { + $target = $target || $('.note.is-editting:visible'); + var selector = this.getEditFormSelector($target); + var $editForm = $(selector); + + $editForm.insertBefore('.notes-form'); + $editForm.find('.js-comment-button').enable(); + $editForm.find('.js-edit-warning').hide(); + }; + + Notes.prototype.getEditFormSelector = function($el) { + var selector = '.note-edit-form:not(.mr-note-edit-form)'; + + if ($el.parents('#diffs').length) { + selector = '.note-edit-form.mr-note-edit-form'; + } + + return selector; + }; + Notes.prototype.removeNoteEditForm = function(note) { - var form; - form = note.find(".current-note-edit-form"); - note.removeClass("is-editting"); - form.removeClass("current-note-edit-form"); + var form = note.find('.current-note-edit-form'); + note.removeClass('is-editting'); + form.removeClass('current-note-edit-form'); + form.find('.js-edit-warning').hide(); // Replace markdown textarea text with original note text. - return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); + return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); }; @@ -671,7 +724,7 @@ */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; e.preventDefault(); $link = $(e.currentTarget); row = $link.closest("tr"); @@ -833,15 +886,46 @@ Notes.prototype.initTaskList = function() { this.enableTaskList(); - return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList); + return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this)); }; Notes.prototype.enableTaskList = function() { return $('.note .js-task-list-container').taskList('enable'); }; - Notes.prototype.updateTaskList = function() { - return $('form', this).submit(); + Notes.prototype.putEditFormInPlace = function($el) { + var $editForm = $(this.getEditFormSelector($el)); + var $note = $el.closest('.note'); + + $editForm.insertAfter($note.find('.note-text')); + + var $originalContentEl = $note.find('.original-note-content'); + var originalContent = $originalContentEl.text().trim(); + var postUrl = $originalContentEl.data('post-url'); + var targetId = $originalContentEl.data('target-id'); + var targetType = $originalContentEl.data('target-type'); + + new GLForm($editForm.find('form')); + + $editForm.find('form') + .attr('action', postUrl) + .attr('data-remote', 'true'); + $editForm.find('.js-form-target-id').val(targetId); + $editForm.find('.js-form-target-type').val(targetType); + $editForm.find('.js-note-text').focus().val(originalContent); + $editForm.find('.js-md-write-button').trigger('click'); + $editForm.find('.referenced-users').hide(); + } + + Notes.prototype.updateTaskList = function(e) { + var $target = $(e.target); + var $list = $target.closest('.js-task-list-container'); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $list.closest('.note'); + + this.putEditFormInPlace($list); + $editForm.find('#note_note').val($note.find('.original-task-list').val()); + $('form', $list).submit(); }; Notes.prototype.updateNotesCount = function(updateCount) { diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index b152d26733f7bc0a141aae81c96b4fbd00c2054e..5d0d594073d0a7e8113ffae8cdfd16c0715fe018 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */ +/* global Flash */ + (function() { this.NotificationsDropdown = (function() { function NotificationsDropdown() { @@ -17,7 +19,7 @@ }); $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) { if (data.saved) { - return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html); + return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html); } else { return new Flash('Failed to save new settings', 'alert'); } diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index fb95648e1c74cf5432796f484fc461215ade89c8..0b09ad113a34e9cdaf3adc641715ccb3f06ebcfb 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,6 +1,7 @@ +/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */ + //= require lib/utils/bootstrap_linked_tabs -/* eslint-disable */ ((global) => { class Pipelines { diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 3723aa24942b86b52201bf325a874174ae82b442..89f7e9769342cdfb29d600163eb790660adbac86 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,13 +1,17 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, no-undef, camelcase, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */ + // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // and showing a warning when more than `x` users are referenced. // -(function() { - var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; +(function () { + var lastTextareaPreviewed; + var markdownPreview; + var previewButtonSelector; + var writeButtonSelector; - this.MarkdownPreview = (function() { + window.MarkdownPreview = (function () { function MarkdownPreview() {} // Minimum number of users referenced before triggering a warning @@ -15,75 +19,73 @@ MarkdownPreview.prototype.ajaxCache = {}; - MarkdownPreview.prototype.showPreview = function(form) { - var mdText, preview; - preview = form.find('.js-md-preview'); - mdText = form.find('textarea.markdown-area').val(); + MarkdownPreview.prototype.showPreview = function ($form) { + var mdText; + var preview = $form.find('.js-md-preview'); + if (preview.hasClass('md-preview-loading')) { + return; + } + mdText = $form.find('textarea.markdown-area').val(); + if (mdText.trim().length === 0) { preview.text('Nothing to preview.'); - return this.hideReferencedUsers(form); + this.hideReferencedUsers($form); } else { - preview.text('Loading...'); - return this.renderMarkdown(mdText, (function(_this) { - return function(response) { - preview.html(response.body); - preview.syntaxHighlight(); - return _this.renderReferencedUsers(response.references.users, form); - }; - })(this)); + preview.addClass('md-preview-loading').text('Loading...'); + this.fetchMarkdownPreview(mdText, (function (response) { + preview.removeClass('md-preview-loading').html(response.body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); + }).bind(this)); } }; - MarkdownPreview.prototype.renderMarkdown = function(text, success) { + MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { if (!window.preview_markdown_path) { return; } if (text === this.ajaxCache.text) { - return success(this.ajaxCache.response); + success(this.ajaxCache.response); + return; } - return $.ajax({ + $.ajax({ type: 'POST', url: window.preview_markdown_path, data: { text: text }, dataType: 'json', - success: (function(_this) { - return function(response) { - _this.ajaxCache = { - text: text, - response: response - }; - return success(response); + success: (function (response) { + this.ajaxCache = { + text: text, + response: response }; - })(this) + success(response); + }).bind(this) }); }; - MarkdownPreview.prototype.hideReferencedUsers = function(form) { - var referencedUsers; - referencedUsers = form.find('.referenced-users'); - return referencedUsers.hide(); + MarkdownPreview.prototype.hideReferencedUsers = function ($form) { + $form.find('.referenced-users').hide(); }; - MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { + MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { var referencedUsers; - referencedUsers = form.find('.referenced-users'); + referencedUsers = $form.find('.referenced-users'); if (referencedUsers.length) { if (users.length >= this.referenceThreshold) { referencedUsers.show(); - return referencedUsers.find('.js-referenced-users-count').text(users.length); + referencedUsers.find('.js-referenced-users-count').text(users.length); } else { - return referencedUsers.hide(); + referencedUsers.hide(); } } }; return MarkdownPreview; + }()); - })(); - - markdownPreview = new MarkdownPreview(); + markdownPreview = new window.MarkdownPreview(); previewButtonSelector = '.js-md-preview-button'; @@ -91,19 +93,14 @@ lastTextareaPreviewed = null; - $.fn.setupMarkdownPreview = function() { - var $form, form_textarea; - $form = $(this); - form_textarea = $form.find('textarea.markdown-area'); - form_textarea.on('input', function() { - return markdownPreview.hideReferencedUsers($form); - }); - return form_textarea.on('blur', function() { - return markdownPreview.showPreview($form); + $.fn.setupMarkdownPreview = function () { + var $form = $(this); + $form.find('textarea.markdown-area').on('input', function () { + markdownPreview.hideReferencedUsers($form); }); }; - $(document).on('markdown-preview:show', function(e, $form) { + $(document).on('markdown-preview:show', function (e, $form) { if (!$form) { return; } @@ -114,10 +111,10 @@ // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); - return markdownPreview.showPreview($form); + markdownPreview.showPreview($form); }); - $(document).on('markdown-preview:hide', function(e, $form) { + $(document).on('markdown-preview:hide', function (e, $form) { if (!$form) { return; } @@ -128,34 +125,33 @@ // toggle content $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); - return $form.find('.md-preview-holder').hide(); + $form.find('.md-preview-holder').hide(); }); - $(document).on('markdown-preview:toggle', function(e, keyboardEvent) { + $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { var $target; $target = $(keyboardEvent.target); if ($target.is('textarea.markdown-area')) { $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - return keyboardEvent.preventDefault(); + keyboardEvent.preventDefault(); } else if (lastTextareaPreviewed) { $target = lastTextareaPreviewed; $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - return keyboardEvent.preventDefault(); + keyboardEvent.preventDefault(); } }); - $(document).on('click', previewButtonSelector, function(e) { + $(document).on('click', previewButtonSelector, function (e) { var $form; e.preventDefault(); $form = $(this).closest('form'); - return $(document).triggerHandler('markdown-preview:show', [$form]); + $(document).triggerHandler('markdown-preview:show', [$form]); }); - $(document).on('click', writeButtonSelector, function(e) { + $(document).on('click', writeButtonSelector, function (e) { var $form; e.preventDefault(); $form = $(this).closest('form'); - return $(document).triggerHandler('markdown-preview:hide', [$form]); + $(document).triggerHandler('markdown-preview:hide', [$form]); }); - -}).call(this); +}()); diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index 6da6c1d02954d2d6ca9484d74bb20c90e40f79b9..b4b6da41f6321bbbd87b15b921945eeeb4096ce7 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */ + ((global) => { // Matches everything but the file name diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index 3eb81808bd62c9ec90ee2a1bc88f36d3db2e255a..aef2e3a3fa8846bc0bf0f8c96ccff892bb81d175 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */ +/* global Flash */ + ((global) => { class Profile { @@ -39,15 +41,12 @@ } beforeUpdateUsername() { - $('.loading-username').show(); - $(this).find('.update-success').hide(); - return $(this).find('.update-failed').hide(); + $('.loading-username', this).removeClass('hidden'); } afterUpdateUsername() { - $('.loading-username').hide(); - $(this).find('.btn-save').enable(); - return $(this).find('.loading-gif').hide(); + $('.loading-username', this).addClass('hidden'); + $('button[type=submit]', this).enable(); } onUpdateNotifs(e, data) { diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 016d999d77ee4675003ba90be7da9509481dfa3e..fcf3a4af9566d35bc3a5c2ae65c405e4763b2fa3 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-undef, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* global Cookies */ +/* global Turbolinks */ +/* global ProjectSelect */ + (function() { this.Project = (function() { function Project() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 804306a329350844b2bb0644d16347cd60297b4d..1bd232314d0d989c9e31d38704c437b5ef07e450 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, no-undef, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */ +/* global fuzzaldrinPlus */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index c99e55234cfcc69f74a5d3cfa31f3e43e0f2ba92..02dafcfb8652438882ae9cb9860243f6be69e2da 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */ +/* global Turbolinks */ + (function() { this.ProjectImport = (function() { function ProjectImport() { diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6 index 03a115cb35b2c51a7c714296f0ea75681c35035f..b8d6a1989963b237c5e47e088d3098ae206c7e3a 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */ + (function(global) { class ProjectLabelSubscription { constructor(container) { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index fe1f96872f37c0b42e507fbaa0279e22cd29b2b3..38bc2e1c3a0e0ba8b8600fffa41d85fc7062bf15 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-undef, no-else-return, quotes, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */ +/* global Api */ + (function() { this.ProjectSelect = (function() { function ProjectSelect() { @@ -13,6 +15,7 @@ }, data: function(term, callback) { var finalCallback, projectsCallback; + var orderBy = $dropdown.data('order-by'); finalCallback = function(projects) { return callback(projects); }; @@ -32,7 +35,7 @@ if (this.groupId) { return Api.groupProjects(this.groupId, term, projectsCallback); } else { - return Api.projects(term, this.orderBy, projectsCallback); + return Api.projects(term, orderBy, projectsCallback); } }, url: function(project) { diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index dbf530bed41a1e134cd8786e3c1e33267f02b581..4548dc68fe1f532b251eda07e93f41c7c1b29ca2 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ + (function() { - this.ProjectsList = { + window.ProjectsList = { init: function() { $(".projects-list-filter").off('keyup'); this.initSearch(); @@ -9,7 +10,7 @@ initSearch: function() { var debounceFilter, projectsListFilter; projectsListFilter = $('.projects-list-filter'); - debounceFilter = _.debounce(ProjectsList.filterResults, 500); + debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); return projectsListFilter.on('keyup', function(e) { if (projectsListFilter.val() !== '') { return debounceFilter(); diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 index 2d60947a666e29dd114a839ce85a99906dfd0b4f..4aef1c84b569d5bc474ee6bebf94114f8c784ff1 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 index c45c9d8ff227a50b59545e6670b1e4e9390c6c86..f26fba979a4c50d16221354f511d5bae80944c60 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */ +/* global ProtectedBranchDropdown */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index e3f226e9a2a3b537e4d6ac16f8ee06b8db40aef7..03f4531abf5212bc2475dab219c16bbadcf0a5b7 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ + class ProtectedBranchDropdown { constructor(options) { this.onSelect = options.onSelect; @@ -75,3 +76,5 @@ class ProtectedBranchDropdown { this.$dropdownFooter.toggleClass('hidden', !branchName); } } + +window.ProtectedBranchDropdown = ProtectedBranchDropdown; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index ac3142ffb07a97f82c4eccfcdd0b96350e639dd2..4ff2fa5a80f758f881e91109c7943e48ded3067a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */ +/* global Flash */ + (global => { global.gl = global.gl || {}; @@ -33,7 +35,7 @@ const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 index 705378a364d0fddd27afd38b94ae19d6d0b9d803..b6972ef2e16233e4dcf2f81a75d43777df82fdcf 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 17e3416383162ad82c2acff987004c6e185fda5b..15b3affd4696bc6533e42a36ff8168b351f137c1 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,2 +1 @@ -/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js new file mode 100644 index 0000000000000000000000000000000000000000..bbb2f186655e6ad0ea3ba6e46fa2c7ef74d004c7 --- /dev/null +++ b/app/assets/javascripts/render_gfm.js @@ -0,0 +1,16 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +// Render Gitlab flavoured Markdown +// +// Delegates to syntax highlight and render math +// +(function() { + $.fn.renderGFM = function() { + this.find('.js-syntax-highlight').syntaxHighlight(); + this.find('.js-render-math').renderMath(); + }; + + $(document).on('ready page:load', function() { + return $('body').renderGFM(); + }); + +}).call(this); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js new file mode 100644 index 0000000000000000000000000000000000000000..209e7a8661d5da3656a997c4c01da828ce9afa19 --- /dev/null +++ b/app/assets/javascripts/render_math.js @@ -0,0 +1,55 @@ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */ +// Renders math using KaTeX in any element with the +// `js-render-math` class +// +// ### Example Markup +// +// <code class="js-render-math"></div> +// +(function() { + // Only load once + var katexLoaded = false; + + // Loop over all math elements and render math + var renderWithKaTeX = function (elements) { + elements.each(function () { + var mathNode = $('<span></span>'); + var $this = $(this); + + var display = $this.attr('data-math-style') === 'display'; + try { + katex.render($this.text(), mathNode.get(0), { displayMode: display }); + mathNode.insertAfter($this); + $this.remove(); + } catch (err) { + // What can we do?? + console.log(err.message); + } + }); + }; + + $.fn.renderMath = function() { + var $this = this; + if ($this.length === 0) return; + + if (katexLoaded) renderWithKaTeX($this); + else { + // Request CSS file so it is in the cache + $.get(gon.katex_css_url, function() { + var css = $('<link>', + { rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }); + css.appendTo('head'); + + // Load KaTeX js + $.getScript(gon.katex_js_url, function() { + katexLoaded = true; + renderWithKaTeX($this); // Run KaTeX + }); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 440b5da756db66001d28a57417ec88e04e8e5ff6..b1e844b730268e1b72db040077089edcac3b679c 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, no-undef, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* global Cookies */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 1d208f1494ca9b2621dedcfc7b786969f34ca92d..5945cab4cf01c1e523a8973ed936a5690e3ace42 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* global Api */ + (function() { this.Search = (function() { function Search() { @@ -10,6 +12,9 @@ selectable: true, filterable: true, fieldName: 'group_id', + search: { + fields: ['name'] + }, data: function(term, callback) { return Api.groups(term, {}, function(data) { data.unshift({ @@ -38,6 +43,9 @@ selectable: true, filterable: true, fieldName: 'project_id', + search: { + fields: ['name'] + }, data: function(term, callback) { return Api.projects(term, 'id', function(data) { data.unshift({ diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index 5fa94556501560a74c0fa081a2bee0f5a8654cea..cec8856d4e758142e24d358fda4ccdf1c1181534 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */ + ((global) => { const KEYCODE = { @@ -141,8 +142,9 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; userId = gon.current_user_id; + userName = gon.current_username; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; if (utils.isInGroupsPage() && groupOptions) { options = groupOptions[utils.getGroupSlug()]; @@ -157,10 +159,10 @@ header: "" + name }, { text: 'Issues assigned to me', - url: issuesPath + "/?assignee_id=" + userId + url: issuesPath + "/?assignee_username=" + userName }, { text: "Issues I've created", - url: issuesPath + "/?author_id=" + userId + url: issuesPath + "/?author_username=" + userName }, 'separator', { text: 'Merge requests assigned to me', url: mrPath + "/?assignee_id=" + userId diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index fa2168723bed0424dc460758dddf62f019f1503d..5ea00f408f49f8bce37a541b657d3f6d17cffa38 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* global Mousetrap */ +/* global Turbolinks */ +/* global findFileURL */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index 65305b8c22fc809d7244b23d88c75d469c4af08c..c26903038b4ce04f0750e881551be09cdc541a43 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */ +/* global Shortcuts */ +/* global Mousetrap */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 1b9a265ba39041a4cb0fa28947c23ed9cd153b8f..4549742bbcb54478fa03c77edd3c65b406754e79 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* global Mousetrap */ +/* global Shortcuts */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 68cd6fad04e6902152050f8054793dcc8a160ac0..3a81380eef0a38e7ec858d10416800e4e8981f17 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index c4899f3566a786ac4190d9515bc6753326ffcae7..b892fbc33933c42e698433e0ee9a84edd9d72e81 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */ +/* global Mousetrap */ +/* global Turbolinks */ +/* global ShortcutsNavigation */ +/* global sidebar */ /*= require mousetrap */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 7d4d6364c706d4c90ec2d5265b7c16e274ccc4c4..0776d0a9b67348fc091b3fcee988e3b9a219498f 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* global Mousetrap */ +/* global Shortcuts */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index a4095d2c06b07c75789f7daac5285d0465b3abdd..ecc3fab84c35d4697b1a41bb1c993bee1c7c949c 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index a23ca449c4b32913ed0408832622b52dd85c2ead..9790a44972d6c98025c4c44c271350f7fe017cd5 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */ +/* global Cookies */ + ((global) => { let singleton; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 0d48e69cce9acd1f81044bb086ccee6856c5b389..9602526063e588b8a00dbb0e94fee28e88c0d3aa 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, no-undef, max-len */ +/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.SingleFileDiff = (function() { + window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; @@ -13,8 +14,7 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; - function SingleFileDiff(file, forceLoad, cb) { - var clickTarget; + function SingleFileDiff(file) { this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -32,14 +32,13 @@ this.content.after(this.collapsedContent); this.$toggleIcon.addClass('fa-caret-down'); } - clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); - if (forceLoad) { - this.toggleDiff({ target: clickTarget }, cb); - } + + $('.file-title, .click-to-expand', this.file).on('click', (function (e) { + this.toggleDiff($(e.target)); + }).bind(this)); } - SingleFileDiff.prototype.toggleDiff = function(e, cb) { - var $target = $(e.target); + SingleFileDiff.prototype.toggleDiff = function($target, cb) { if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.isOpen = !this.isOpen; if (!this.isOpen && !this.hasError) { @@ -90,10 +89,10 @@ })(); - $.fn.singleFileDiff = function(forceLoad, cb) { + $.fn.singleFileDiff = function() { return this.each(function() { - if (!$.data(this, 'singleFileDiff') || forceLoad) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); + if (!$.data(this, 'singleFileDiff')) { + return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); } }); }; diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 2c8ecba7de430e90ad3d558c8d318f81cf708c8a..18512d179b3423c3fe66348991702319dc7b1b91 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, no-undef, quotes, semi, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */ +/* global ace */ + /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 index c3afc3f2246f2fa1acf7cc9740a7cf2abdbad41f..6f913326a3adc31acea7c3302ef4548725be08d6 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */ + (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 32803fa790b4ef5caf8583d47f45647230c62598..f1fc526bf2e015fd7aed1425877ef3a5542f6c9f 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */ +/* global Flash */ + (function() { this.Star = (function() { function Star() { diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 932120157a34c418e3732a1123a87b8511435026..d81916051284986d5a7bd115d093a1b02abe5a8e 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -1,6 +1,3 @@ -//= require vue -//= require vue-resource - (() => { /* * SubbableResource can be extended to provide a pubsub-style service for one-off REST diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index bd37d69165f692fcb2cb0ee376a4bb381f7b1324..5d0fa62c50a00425ce5fc5ea3650e6726f7c4115 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ + // Syntax Highlighter // // Applies a syntax highlighting color scheme CSS class to any element with the @@ -9,8 +10,10 @@ // <div class="js-syntax-highlight"></div> // (function() { + $.fn.syntaxHighlight = function() { var $children; + if ($(this).hasClass('js-syntax-highlight')) { // Given the element itself, apply highlighting return $(this).addClass(gon.user_color_scheme); @@ -23,8 +26,4 @@ } }; - $(document).on('ready page:load', function() { - return $('.js-syntax-highlight').syntaxHighlight(); - }); - }).call(this); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index 93a3d67ee9fb1ed80a757403e41e1e71700f3d3c..bdec948fb63f66ca2b39684700e822c52d3b1d7f 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */ +/* global Api */ + /*= require ../blob/template_selector */ ((global) => { @@ -21,7 +23,7 @@ }); $('.no-template', this.dropdown.parent()).on('click', () => { - this.currentTemplate = ''; + this.currentTemplate.content = ''; this.setInputValueToTemplateContent(); $('.dropdown-toggle-text', this.dropdown).text('Choose a template'); }); diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index 0a3890e85feeb0fae6a3cd1a7f6bee21d7b6a552..7310b9de074e50e86b06f119259a11d018ccf10d 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */ + ((global) => { class IssuableTemplateSelectors { constructor({ $dropdowns, editor } = {}) { diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..6b9422b1816194c452743efd29d3405fe937d837 --- /dev/null +++ b/app/assets/javascripts/terminal/terminal.js.es6 @@ -0,0 +1,62 @@ +/* global Terminal */ + +(() => { + class GLTerminal { + + constructor(options) { + this.options = options || {}; + + this.options.cursorBlink = options.cursorBlink || true; + this.options.screenKeys = options.screenKeys || true; + this.container = document.querySelector(options.selector); + + this.setSocketUrl(); + this.createTerminal(); + $(window).off('resize.terminal').on('resize.terminal', () => { + this.terminal.fit(); + }); + } + + setSocketUrl() { + const { protocol, hostname, port } = window.location; + const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://'; + const path = this.container.dataset.projectPath; + + this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`; + } + + createTerminal() { + this.terminal = new Terminal(this.options); + this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']); + this.socket.binaryType = 'arraybuffer'; + + this.terminal.open(this.container); + this.socket.onopen = () => { this.runTerminal(); }; + this.socket.onerror = () => { this.handleSocketFailure(); }; + } + + runTerminal() { + const decoder = new TextDecoder('utf-8'); + const encoder = new TextEncoder('utf-8'); + + this.terminal.on('data', (data) => { + this.socket.send(encoder.encode(data)); + }); + + this.socket.addEventListener('message', (ev) => { + this.terminal.write(decoder.decode(ev.data)); + }); + + this.isTerminalInitialized = true; + this.terminal.fit(); + } + + handleSocketFailure() { + this.terminal.write('\r\nConnection failure'); + } + + } + + window.gl = window.gl || {}; + gl.Terminal = GLTerminal; +})(); diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..33d2c1e1a17e708bd328b82bd8830216827232cd --- /dev/null +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -0,0 +1,7 @@ +//= require xterm/encoding-indexes +//= require xterm/encoding +//= require xterm/xterm.js +//= require xterm/fit.js +//= require ./terminal.js + +$(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 213e80825b726137ade2b32a563b619be6e1e09b..d8713600030e71444a97ef1b963100e7c4dcbc02 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */ +/* global UsersSelect */ +/* global Turbolinks */ + ((global) => { class Todos { @@ -72,7 +75,7 @@ allDoneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $target = $(e.currentTarget); + const $target = $(e.currentTarget); $target.disable(); return $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js.es6 similarity index 75% rename from app/assets/javascripts/u2f/authenticate.js rename to app/assets/javascripts/u2f/authenticate.js.es6 index 5d991542b512f8e98f35404055fe888e22494020..2b992109a8c4997373974a51339e8ed7c9d35e4b 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js.es6 @@ -1,24 +1,33 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global u2f */ +/* global U2FError */ +/* global U2FUtil */ + // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server // State Flow #2: setup -> in_progress -> error -> setup (function() { + const global = window.gl || (window.gl = {}); + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - this.U2FAuthenticate = (function() { - function U2FAuthenticate(container, u2fParams) { + global.U2FAuthenticate = (function() { + function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) { this.container = container; this.renderNotSupported = bind(this.renderNotSupported, this); this.renderAuthenticated = bind(this.renderAuthenticated, this); this.renderError = bind(this.renderError, this); this.renderInProgress = bind(this.renderInProgress, this); - this.renderSetup = bind(this.renderSetup, this); this.renderTemplate = bind(this.renderTemplate, this); this.authenticate = bind(this.authenticate, this); this.start = bind(this.start, this); this.appId = u2fParams.app_id; this.challenge = u2fParams.challenge; + this.form = form; + this.fallbackButton = fallbackButton; + this.fallbackUI = fallbackUI; + if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); this.signRequests = u2fParams.sign_requests.map(function(request) { // The U2F Javascript API v1.1 requires a single challenge, with // _no challenges per-request_. The U2F Javascript API v1.0 requires a @@ -37,7 +46,7 @@ U2FAuthenticate.prototype.start = function() { if (U2FUtil.isU2FSupported()) { - return this.renderSetup(); + return this.renderInProgress(); } else { return this.renderNotSupported(); } @@ -73,11 +82,6 @@ return this.container.html(template(params)); }; - U2FAuthenticate.prototype.renderSetup = function() { - this.renderTemplate('setup'); - return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress); - }; - U2FAuthenticate.prototype.renderInProgress = function() { this.renderTemplate('inProgress'); return this.authenticate(); @@ -85,24 +89,32 @@ U2FAuthenticate.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); - return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress); }; U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { this.renderTemplate('authenticated'); - // Prefer to do this instead of interpolating using Underscore templates - // because of JSON escaping issues. - return this.container.find("#js-device-response").val(deviceResponse); + const container = this.container[0]; + container.querySelector('#js-device-response').value = deviceResponse; + container.querySelector(this.form).submit(); + this.fallbackButton.classList.add('hidden'); }; U2FAuthenticate.prototype.renderNotSupported = function() { return this.renderTemplate('notSupported'); }; + U2FAuthenticate.prototype.switchToFallbackUI = function() { + this.fallbackButton.classList.add('hidden'); + this.container[0].classList.add('hidden'); + this.fallbackUI.classList.remove('hidden'); + }; + return U2FAuthenticate; })(); -}).call(this); +})(); diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 4c70a6e9bb6e727a6ba67d0bfc0295a94c36ae5e..bb9942a3aa01e58bcd774bfb935d246d29286c1f 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, no-undef, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */ +/* global u2f */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -7,7 +9,6 @@ this.errorCode = errorCode; this.message = bind(this.message, this); this.httpsDisabled = window.location.protocol !== 'https:'; - console.error("U2F Error Code: " + this.errorCode); } U2FError.prototype.message = function() { diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 97d8993cac22dc6e5b81399985f5df104e7e32f6..050c9bfc02e3cac644aa250ec86dc71c3b519d65 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global u2f */ +/* global U2FError */ +/* global U2FUtil */ + // Register U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> registered -> POST to server @@ -72,7 +76,8 @@ U2FRegister.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); }; diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 5e869e99fdb1a657c26f4b3064861ce6c37a7869..0a2db7c05fe18b80ae9863fd48958b3166a7a90d 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */ +/* global Cookies */ + ((global) => { global.User = class { constructor({ action }) { diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 5a625611987aa0407ac4b073c20812e4b1eb899f..b9c23b51b4da02c577954080b72b71c9e50fd397 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */ + /* UserTabs diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index c4dde575c6ee88ec685df0e74e98aaea028abb8c..137cefa3b8e872d77106ca4159cd12970ce73205 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ + ((global) => { const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; @@ -77,7 +78,7 @@ this.renderState(); return $.ajax({ type: 'GET', - url: `/users/${username}/exists`, + url: `${gon.relative_url_root}/users/${username}/exists`, dataType: 'json', success: (res) => this.setAvailabilityState(res.exists) }); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index ba7f533c3490275c7d769dfc963df99f11d93138..578be7c3590a2360df865092637341624dbe5319 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, no-undef, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* global d3 */ +/* global dateFormat */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index c6e18fad832284e0857da34530a6439648dda198..d4b5e03aa358dd531966544cc70feb0f14ec5e2f 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,4 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, no-undef, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */ +/* global Vue */ +/* global Issuable */ +/* global ListUser */ + (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice; diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..605824fa93988aa3b075a90fe75f81da7f7d1376 --- /dev/null +++ b/app/assets/javascripts/vue_pagination/index.js.es6 @@ -0,0 +1,148 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign, no-plusplus */ + +((gl) => { + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = 'Prev'; + const NEXT = 'Next'; + const FIRST = '<< First'; + const LAST = 'Last >>'; + + gl.VueGlPagination = Vue.extend({ + props: { + + /** + This function will take the information given by the pagination component + And make a new Turbolinks call + + Here is an example `change` method: + + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + */ + + change: { + type: Function, + required: true, + }, + + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + + pageInfo: { + type: Object, + required: true, + }, + }, + methods: { + changePage(e) { + let apiScope = gl.utils.getParameterByName('scope'); + + if (!apiScope) apiScope = 'all'; + + const text = e.target.innerText; + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages, apiScope); + break; + case NEXT: + this.change(nextPage, apiScope); + break; + case PREV: + this.change(previousPage, apiScope); + break; + case FIRST: + this.change(1, apiScope); + break; + default: + this.change(+text, apiScope); + break; + } + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) items.push({ title: FIRST }); + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i++) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) items.push({ title: LAST, last: true }); + + return items; + }, + }, + template: ` + <div class="gl-pagination"> + <ul class="pagination clearfix"> + <li v-for='item in getItems' + :class='{ + page: item.page, + prev: item.prev, + next: item.next, + separator: item.separator, + active: item.active, + disabled: item.disabled + }' + > + <a @click="changePage($event)">{{item.title}}</a> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..edd01f17a9748435a78b8abf4e7a30e42400a19b --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -0,0 +1,42 @@ +/* global Vue, VueResource, gl */ +/*= require vue_common_component/commit */ +/*= require vue_pagination/index */ +/*= require vue-resource +/*= require boards/vue_resource_interceptor */ +/*= require ./status.js.es6 */ +/*= require ./store.js.es6 */ +/*= require ./pipeline_url.js.es6 */ +/*= require ./stage.js.es6 */ +/*= require ./stages.js.es6 */ +/*= require ./pipeline_actions.js.es6 */ +/*= require ./time_ago.js.es6 */ +/*= require ./pipelines.js.es6 */ + +(() => { + const project = document.querySelector('.pipelines'); + const entry = document.querySelector('.vue-pipelines-index'); + const svgs = document.querySelector('.pipeline-svgs'); + + Vue.use(VueResource); + + if (!entry) return null; + return new Vue({ + el: entry, + data: { + scope: project.dataset.url, + store: new gl.PipelineStore(), + svgs: svgs.dataset, + }, + components: { + 'vue-pipelines': gl.VuePipelines, + }, + template: ` + <vue-pipelines + :scope='scope' + :store='store' + :svgs='svgs' + > + </vue-pipelines> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..ad5cb30cc42691d8d002281ff50b697a8ffcb55b --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -0,0 +1,99 @@ +/* global Vue, Flash, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelineActions = Vue.extend({ + props: ['pipeline', 'svgs'], + computed: { + actions() { + return this.pipeline.details.manual_actions.length > 0; + }, + artifacts() { + return this.pipeline.details.artifacts.length > 0; + }, + }, + methods: { + download(name) { + return `Download ${name} artifacts`; + }, + }, + template: ` + <td class="pipeline-actions hidden-xs"> + <div class="controls pull-right"> + <div class="btn-group inline"> + <div class="btn-group"> + <a + v-if='actions' + class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions" + data-toggle="dropdown" + title="Manual build" + alt="Manual Build" + > + <span v-html='svgs.iconPlay'></span> + <i class="fa fa-caret-down"></i> + </a> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for='action in pipeline.details.manual_actions'> + <a + rel="nofollow" + data-method="post" + :href='action.path' + title="Manual build" + > + <span v-html='svgs.iconPlay'></span> + <span title="Manual build">{{action.name}}</span> + </a> + </li> + </ul> + </div> + <div class="btn-group"> + <a + v-if='artifacts' + class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" + data-toggle="dropdown" + type="button" + > + <i class="fa fa-download"></i> + <i class="fa fa-caret-down"></i> + </a> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for='artifact in pipeline.details.artifacts'> + <a + rel="nofollow" + :href='artifact.path' + > + <i class="fa fa-download"></i> + <span>{{download(artifact.name)}}</span> + </a> + </li> + </ul> + </div> + </div> + <div class="cancel-retry-btns inline"> + <a + v-if='pipeline.flags.retryable' + class="btn has-tooltip" + title="Retry" + rel="nofollow" + data-method="post" + :href='pipeline.retry_path' + > + <i class="fa fa-repeat"></i> + </a> + <a + v-if='pipeline.flags.cancelable' + class="btn btn-remove has-tooltip" + title="Cancel" + rel="nofollow" + data-method="post" + :href='pipeline.cancel_path' + data-original-title="Cancel" + > + <i class="fa fa-remove"></i> + </a> + </div> + </div> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..ae5649f0519d67e477c2e08633093bf200ddfb8b --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 @@ -0,0 +1,63 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelineUrl = Vue.extend({ + props: [ + 'pipeline', + ], + computed: { + user() { + return !!this.pipeline.user; + }, + }, + template: ` + <td> + <a :href='pipeline.path'> + <span class="pipeline-id">#{{pipeline.id}}</span> + </a> + <span>by</span> + <a + v-if='user' + :href='pipeline.user.web_url' + > + <img + v-if='user' + class="avatar has-tooltip s20 " + :title='pipeline.user.name' + data-container="body" + :src='pipeline.user.avatar_url' + > + </a> + <span + v-if='!user' + class="api monospace" + > + API + </span> + <span + v-if='pipeline.flags.latest' + class="label label-success has-tooltip" + title="Latest pipeline for this branch" + data-original-title="Latest pipeline for this branch" + > + latest + </span> + <span + v-if='pipeline.flags.yaml_errors' + class="label label-danger has-tooltip" + :title='pipeline.yaml_errors' + :data-original-title='pipeline.yaml_errors' + > + yaml invalid + </span> + <span + v-if='pipeline.flags.stuck' + class="label label-warning" + > + stuck + </span> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..b2ed05503c9d3b836ec072075850663e67785c43 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -0,0 +1,131 @@ +/* global Vue, Turbolinks, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelines = Vue.extend({ + components: { + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + stages: gl.VueStages, + commit: gl.CommitComponent, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + glPagination: gl.VueGlPagination, + statusScope: gl.VueStatusScope, + timeAgo: gl.VueTimeAgo, + }, + data() { + return { + pipelines: [], + timeLoopInterval: '', + intervalId: '', + apiScope: 'all', + pageInfo: {}, + pagenum: 1, + count: { all: 0, running_or_pending: 0 }, + pageRequest: false, + }; + }, + props: ['scope', 'store', 'svgs'], + created() { + const pagenum = gl.utils.getParameterByName('p'); + const scope = gl.utils.getParameterByName('scope'); + if (pagenum) this.pagenum = pagenum; + if (scope) this.apiScope = scope; + this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); + }, + methods: { + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + ref(pipeline) { + const { ref } = pipeline; + return { name: ref.name, tag: ref.tag, ref_url: ref.path }; + }, + commitTitle(pipeline) { + return pipeline.commit ? pipeline.commit.title : ''; + }, + commitSha(pipeline) { + return pipeline.commit ? pipeline.commit.short_id : ''; + }, + commitUrl(pipeline) { + return pipeline.commit ? pipeline.commit.commit_path : ''; + }, + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + template: ` + <div> + <div class="pipelines realtime-loading" v-if='pipelines.length < 1'> + <i class="fa fa-spinner fa-spin"></i> + </div> + <div class="table-holder" v-if='pipelines.length'> + <table class="table ci-table"> + <thead> + <tr> + <th class="pipeline-status">Status</th> + <th class="pipeline-info">Pipeline</th> + <th class="pipeline-commit">Commit</th> + <th class="pipeline-stages">Stages</th> + <th class="pipeline-date"></th> + <th class="pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <tr class="commit" v-for='pipeline in pipelines'> + <status-scope + :pipeline='pipeline' + :match='match' + :svgs='svgs' + > + </status-scope> + <pipeline-url :pipeline='pipeline'></pipeline-url> + <td> + <commit + :commit-icon-svg='svgs.commitIconSvg' + :author='author(pipeline)' + :tag="pipeline.ref.tag" + :title='commitTitle(pipeline)' + :commit-ref='ref(pipeline)' + :short-sha='commitSha(pipeline)' + :commit-url='commitUrl(pipeline)' + > + </commit> + </td> + <stages + :pipeline='pipeline' + :svgs='svgs' + :match='match' + > + </stages> + <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> + <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> + </tr> + </tbody> + </table> + </div> + <div class="pipelines realtime-loading" v-if='pageRequest'> + <i class="fa fa-spinner fa-spin"></i> + </div> + <gl-pagination + v-if='pageInfo.total > pageInfo.perPage' + :pagenum='pagenum' + :change='change' + :count='count.all' + :pageInfo='pageInfo' + > + </gl-pagination> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..f075a995846870636bb0e7f76468cf1cee49ee57 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -0,0 +1,69 @@ +/* global Vue, Flash, gl */ +/* eslint-disable no-param-reassign, no-bitwise */ + +((gl) => { + gl.VueStage = Vue.extend({ + data() { + return { + count: 0, + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }; + }, + props: ['stage', 'svgs', 'match'], + methods: { + fetchBuilds() { + if (this.count > 0) return null; + return this.$http.get(this.stage.dropdown_path) + .then((response) => { + this.count += 1; + this.builds = JSON.parse(response.body).html; + }, () => { + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + }, + computed: { + buildsOrSpinner() { + return this.builds ? this.builds : this.spinner; + }, + dropdownClass() { + if (this.builds) return 'js-builds-dropdown-container'; + return 'js-builds-dropdown-loading builds-dropdown-loading'; + }, + buildStatus() { + return `Build: ${this.stage.status.label}`; + }, + tooltip() { + return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; + }, + svg() { + const icon = this.stage.status.icon; + const stageIcon = icon.replace(/icon/i, 'stage_icon'); + return this.svgs[this.match(stageIcon)]; + }, + triggerButtonClass() { + return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; + }, + }, + template: ` + <div> + <button + @click='fetchBuilds' + :class="triggerButtonClass" + :title='stage.title' + data-placement="top" + data-toggle="dropdown" + type="button"> + <span v-html="svg"></span> + <i class="fa fa-caret-down "></i> + </button> + <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> + <div class="arrow-up"></div> + <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..cb176b3f0c6e04ef9b8528d8252a30b8f4058749 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 @@ -0,0 +1,21 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStages = Vue.extend({ + components: { + 'vue-stage': gl.VueStage, + }, + props: ['pipeline', 'svgs', 'match'], + template: ` + <td class="stage-cell"> + <div + class="stage-container dropdown js-mini-pipeline-graph" + v-for='stage in pipeline.details.stages' + > + <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> + </div> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..05175082704f445618ae124a18ae4fb9cc2a092b --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6 @@ -0,0 +1,34 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStatusScope = Vue.extend({ + props: [ + 'pipeline', 'svgs', 'match', + ], + computed: { + cssClasses() { + const cssObject = { 'ci-status': true }; + cssObject[`ci-${this.pipeline.details.status.group}`] = true; + return cssObject; + }, + svg() { + return this.svgs[this.match(this.pipeline.details.status.icon)]; + }, + detailsPath() { + const { status } = this.pipeline.details; + return status.has_details ? status.details_path : false; + }, + }, + template: ` + <td class="commit-link"> + <a + :class='cssClasses' + :href='detailsPath' + v-html='svg + pipeline.details.status.text' + > + </a> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..1982142853a6b2dbcd0c9d3e483886d9719f80c0 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -0,0 +1,69 @@ +/* global gl, Flash */ +/* eslint-disable no-param-reassign, no-underscore-dangle */ +/*= require vue_realtime_listener/index.js */ + +((gl) => { + const pageValues = (headers) => { + const normalizedHeaders = {}; + + Object.keys(headers).forEach((e) => { + normalizedHeaders[e.toUpperCase()] = headers[e]; + }); + + const paginationInfo = { + perPage: +normalizedHeaders['X-PER-PAGE'], + page: +normalizedHeaders['X-PAGE'], + total: +normalizedHeaders['X-TOTAL'], + totalPages: +normalizedHeaders['X-TOTAL-PAGES'], + nextPage: +normalizedHeaders['X-NEXT-PAGE'], + previousPage: +normalizedHeaders['X-PREV-PAGE'], + }; + + return paginationInfo; + }; + + gl.PipelineStore = class { + fetchDataLoop(Vue, pageNum, url, apiScope) { + const updatePipelineNums = (count) => { + const { all } = count; + const running = count.running_or_pending; + document.querySelector('.js-totalbuilds-count').innerHTML = all; + document.querySelector('.js-running-count').innerHTML = running; + }; + + const goFetch = () => + this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) + .then((response) => { + const pageInfo = pageValues(response.headers); + this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); + + const res = JSON.parse(response.body); + this.count = Object.assign({}, this.count, res.count); + this.pipelines = Object.assign([], this.pipelines, res.pipelines); + + updatePipelineNums(this.count); + this.pageRequest = false; + }, () => { + this.pageRequest = false; + return new Flash('Something went wrong on our end.'); + }); + + goFetch(); + + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children + .filter(e => e.$options._componentTag === 'time-ago') + .forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..655110feba150d198fb72819372f612ec3c86b05 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -0,0 +1,73 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueTimeAgo = Vue.extend({ + data() { + return { + currentTime: new Date(), + }; + }, + props: ['pipeline', 'svgs'], + computed: { + timeAgo() { + return gl.utils.getTimeago(); + }, + localTimeFinished() { + return gl.utils.formatDate(this.pipeline.details.finished_at); + }, + timeStopped() { + const changeTime = this.currentTime; + const options = { + weekday: 'long', + year: 'numeric', + month: 'short', + day: 'numeric', + }; + options.timeZoneName = 'short'; + const finished = this.pipeline.details.finished_at; + if (!finished && changeTime) return false; + return ({ words: this.timeAgo.format(finished) }); + }, + duration() { + const { duration } = this.pipeline.details; + const date = new Date(duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + if (hh < 10) hh = `0${hh}`; + if (mm < 10) mm = `0${mm}`; + if (ss < 10) ss = `0${ss}`; + + if (duration !== null) return `${hh}:${mm}:${ss}`; + return false; + }, + }, + methods: { + changeTime() { + this.currentTime = new Date(); + }, + }, + template: ` + <td> + <p class="duration" v-if='duration'> + <span v-html='svgs.iconTimer'></span> + {{duration}} + </p> + <p class="finished-at" v-if='timeStopped'> + <i class="fa fa-calendar"></i> + <time + data-toggle="tooltip" + data-placement="top" + data-container="body" + :data-original-title='localTimeFinished' + > + {{timeStopped.words}} + </time> + </p> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..23cac1466d22242c8e0097ed0ab79b63a6cb610f --- /dev/null +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -0,0 +1,18 @@ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueRealtimeListener = (removeIntervals, startIntervals) => { + const removeAll = () => { + removeIntervals(); + window.removeEventListener('beforeunload', removeIntervals); + window.removeEventListener('focus', startIntervals); + window.removeEventListener('blur', removeIntervals); + document.removeEventListener('page:fetch', removeAll); + }; + + window.addEventListener('beforeunload', removeIntervals); + window.addEventListener('focus', startIntervals); + window.addEventListener('blur', removeIntervals); + document.addEventListener('page:fetch', removeAll); + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 82eb761442ad1dc72b55d17289d8a29495b18e8d..e09b59dd5aaac7890e5497494787ed6cfae489e5 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, no-undef, camelcase, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ + // Zen Mode (full screen) textarea // /*= provides zen_mode:enter */ diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c82a9a2b9e32abde2c47f560934549176de82fc3..3cf49f4ff1b0998974590ee64116a3f16d049243 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -9,6 +9,7 @@ @import "framework/asciidoctor.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; +@import "framework/badges.scss"; @import "framework/calendar.scss"; @import "framework/callout.scss"; @import "framework/common.scss"; @@ -44,3 +45,6 @@ @import "framework/awards.scss"; @import "framework/images.scss"; @import "framework/broadcast-messages"; +@import "framework/emojis.scss"; +@import "framework/icons.scss"; +@import "framework/snippets.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index f1d36efb3debe7baf54fda5348b41d551293d478..8d38fc78a191321666c32f133670c0e4b6fddd58 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -50,3 +50,77 @@ .pulse { @include webkit-prefix(animation-name, pulse); } + +/* +* General hover animations +*/ + + +// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab +// Usage: @include transition(width, height 0.3s ease-in-out); +// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out); +// transition(width 0.2s, height 0.3s ease-in-out); +// +// Pass in any number of transitions +@mixin transition($transitions...) { + $unfoldedTransitions: (); + @each $transition in $transitions { + $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma); + } + + transition: $unfoldedTransitions; +} + +@function unfoldTransition ($transition) { + // Default values + $property: all; + $duration: $general-hover-transition-duration; + $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want + $delay: null; // Browser default is 0, which is what we want + $defaultProperties: ($property, $duration, $easing, $delay); + + // Grab transition properties if they exist + $unfoldedTransition: (); + @for $i from 1 through length($defaultProperties) { + $p: null; + @if $i <= length($transition) { + $p: nth($transition, $i); + } @else { + $p: nth($defaultProperties, $i); + } + $unfoldedTransition: append($unfoldedTransition, $p); + } + + @return $unfoldedTransition; +} + +.btn, +.side-nav-toggle { + @include transition(background-color, border-color, color, box-shadow); +} + +.dropdown-menu-toggle, +.avatar-circle, +.header-user-avatar { + @include transition(border-color); +} + +.note-action-button .link-highlight, +.toolbar-btn, +.dropdown-toggle-caret, +.fa:not(.fa-bell) { + @include transition(color); +} + +a { + @include transition(background-color, color, border); +} + +.tree-table td, +.well-list > li { + @include transition(background-color, border-color); +} + +.stage-nav-item { + @include transition(background-color, box-shadow); +} diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 000e591e09c8f62a3653f1b9354d0e4e442a05a1..8392b98f0a7cf44ba29e69adeeb45e922cb16897 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -52,6 +52,10 @@ border-radius: 0; border: none; } + + &:not([href]):hover { + border-color: rgba($avatar-border, .2); + } } .identicon { @@ -64,7 +68,7 @@ &.s32 { font-size: 20px; line-height: 30px; } &.s40 { font-size: 16px; line-height: 38px; } &.s60 { font-size: 32px; line-height: 58px; } - &.s70 { font-size: 34px; line-height: 68px; } + &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 88px; } &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 138px; } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index dece5c3202b33d9ea6a328be638fb43681d55438..49907417e2699d7d0f29a26e0e296dcb5be8f030 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -12,8 +12,8 @@ z-index: 9; width: 300px; font-size: 14px; - background-color: $award-emoji-menu-bg; - border: 1px solid $award-emoji-menu-border; + background-color: $white-light; + border: 1px solid $border-white-light; border-radius: $border-radius-base; box-shadow: 0 6px 12px $award-emoji-menu-shadow; pointer-events: none; @@ -97,8 +97,20 @@ padding: 5px 6px; outline: 0; - &:hover, + &.disabled { + cursor: default; + + &:hover, + &:focus, + &:active { + background-color: $white-light; + border-color: $border-color; + box-shadow: none; + } + } + &.active, + &:hover, &:active { background-color: $row-hover; border-color: $row-hover-border; @@ -135,7 +147,7 @@ } .award-control-icon { - color: $award-emoji-new-btn-icon-color; + color: $border-gray-normal; margin-top: 1px; } } diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss new file mode 100644 index 0000000000000000000000000000000000000000..47a8f44c709aa7eea0364ed3dd43b23b161aa132 --- /dev/null +++ b/app/assets/stylesheets/framework/badges.scss @@ -0,0 +1,6 @@ +.badge { + font-weight: normal; + background-color: $badge-bg; + color: $badge-color; + vertical-align: baseline; +} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 95c024992712e52b7a611512f1771cb33042a03a..407c800feb7f1306ec9653ca6f2caa8acc3504b1 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,15 +1,15 @@ .centered-light-block { text-align: center; - color: $gl-gray; + color: $gl-text-color; margin: 20px; } .nothing-here-block { text-align: center; padding: 20px; - color: $gl-gray; + color: $gl-text-color; font-weight: normal; - font-size: 16px; + font-size: 14px; line-height: 36px; &.diff-collapsed { @@ -24,12 +24,12 @@ .row-content-block { margin-top: 0; margin-bottom: -$gl-padding; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; margin-bottom: 0; border-top: 1px solid $white-dark; border-bottom: 1px solid $white-dark; - color: $gl-gray; + color: $gl-text-color; &.oneline-block { line-height: 42px; @@ -118,7 +118,7 @@ .cover-block { text-align: center; - background: $background-color; + background: $gray-light; padding-top: 44px; position: relative; @@ -135,11 +135,11 @@ } .cover-title { - color: $gl-header-color; + color: $gl-text-color; font-size: 23px; h1 { - color: $gl-gray-dark; + color: $gl-text-color; margin-bottom: 6px; font-size: 23px; } @@ -153,7 +153,7 @@ p { padding: 0 $gl-padding; - color: $gl-text-color-dark; + color: $gl-text-color; } } @@ -211,7 +211,7 @@ display: inline; font-weight: normal; font-size: 24px; - color: $gl-title-color; + color: $gl-text-color; } } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 1c7b2f4df7cc01882f52a1548ae9a5ab74cd9916..e04a87a7327fc11cb4b34aa0f880998657b75f32 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -88,11 +88,11 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color); } @mixin btn-white { - @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color); } @mixin btn-with-margin { @@ -230,12 +230,19 @@ } } +.btn-terminal { + svg { + height: 14px; + width: 18px; + } +} + .btn-lg { padding: 12px 20px; } .btn-transparent { - color: $btn-transparent-color; + color: $gl-text-color-secondary; background-color: transparent; border: 0; @@ -289,7 +296,7 @@ .active { box-shadow: $gl-btn-active-background; - border: 1px solid $border-white-dark !important; + border: 1px solid $border-gray-dark !important; background-color: $btn-active-gray-light !important; } } @@ -309,8 +316,8 @@ text-align: left; padding: 6px 16px; border-color: $border-color; - color: $btn-placeholder-gray; - background-color: $background-color; + color: $gray-darkest; + background-color: $gray-light; &:hover, &:active, @@ -318,8 +325,8 @@ cursor: text; box-shadow: none; border-color: $border-color; - color: $btn-placeholder-gray; - background-color: $background-color; + color: $gray-darkest; + background-color: $gray-light; } } @@ -331,7 +338,7 @@ margin-left: 10px; i { - color: $gl-icon-color; + color: $gl-text-color-secondary; } } @@ -344,8 +351,8 @@ } .btn-static { - background-color: $background-color !important; - border: 1px solid $border-gray-light; + background-color: $gray-light !important; + border: 1px solid $border-gray-normal; cursor: default; &:active { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 2a100980aca8a169f1a3344cbdddee8e862fa788..e0e46dd73aff33c18e3868ef3930b20c0962cd26 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -11,7 +11,7 @@ padding: $gl-padding; border-left: 3px solid $border-color; color: $text-color; - background: $background-color; + background: $gray-light; } .bs-callout h4 { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 251e43d2eddb581781563cf96c55330ad4c9c163..0ce94a26a7fefa9fce271febe3202b9f98b28249 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -26,6 +26,7 @@ .append-bottom-default { margin-bottom: $gl-padding; } .inline { display: inline-block; } .center { text-align: center; } +.vertical-align-middle { vertical-align: middle; } .underlined-link { text-decoration: underline; } .hint { font-style: italic; color: $hint-color; } @@ -57,16 +58,33 @@ pre { border-radius: 0; color: $well-pre-color; } + + &.wrap { + word-break: break-word; + white-space: pre-wrap; + } } hr { margin: $gl-padding 0; + border-top: 1px solid darken($gray-normal, 8%); } .str-truncated { @include str-truncated; } +.block-truncated { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + > div, + .str-truncated { + display: inline; + } +} + .item-title { font-weight: 600; } /** FLASH message **/ @@ -394,7 +412,7 @@ table { padding: 0 10px; clip: auto; text-decoration: none; - color: $gl-title-color; + color: $gl-text-color; background: $gray-light; z-index: 1; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d5914b900e217ee5086876cc4a1f5ddf94801fdf..755eddefa42ceee2b2b6eea091da85074aac84ef 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -10,7 +10,7 @@ @mixin chevron-active { .fa-chevron-down { - color: $dropdown-toggle-hover-icon-color; + color: $gray-darkest; } } @@ -28,14 +28,14 @@ .dropdown-toggle, .dropdown-menu-toggle { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; + border-color: $gray-darkest; } } .dropdown-toggle { padding: 6px 8px 6px 10px; - background-color: $dropdown-toggle-bg; - color: $dropdown-toggle-color; + background-color: $white-light; + color: $gl-text-color; font-size: 14px; text-align: left; border: 1px solid $border-color; @@ -73,7 +73,7 @@ } .fa { - color: $dropdown-toggle-icon-color; + color: $gray-darkest; } .fa-chevron-down { @@ -85,7 +85,7 @@ &:hover { @include chevron-active; - border-color: $dropdown-toggle-hover-border-color; + border-color: $gray-darkest; } &:focus:active { @@ -98,7 +98,7 @@ @extend .dropdown-toggle; padding-right: 20px; position: relative; - width: 160px; + width: 163px; text-overflow: ellipsis; overflow: hidden; @@ -131,7 +131,7 @@ font-size: 14px; font-weight: normal; padding: 8px 0; - background-color: $dropdown-bg; + background-color: $white-light; border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; @@ -188,7 +188,6 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; - outline: 0; } &.dropdown-menu-empty-link { @@ -202,7 +201,7 @@ } .icon-play { - fill: $table-text-gray; + fill: $gl-text-color-secondary; margin-right: 6px; height: 12px; width: 11px; @@ -210,7 +209,7 @@ } .dropdown-header { - color: $dropdown-header-color; + color: $gl-text-color-secondary; font-size: 13px; line-height: 22px; padding: 0 10px; @@ -223,7 +222,7 @@ .unclickable { cursor: not-allowed; padding: 5px 8px; - color: $dropdown-header-color; + color: $gl-text-color-secondary; } } @@ -593,7 +592,7 @@ } .ui-datepicker-title { - color: $gl-gray; + color: $gl-text-color; font-size: 14px; line-height: 1; font-weight: normal; @@ -602,30 +601,30 @@ th { padding: 2px 0; - color: $calendar-header-color; + color: $note-disabled-comment-color; font-weight: normal; text-transform: lowercase; border-top: 1px solid $calendar-border-color; } .ui-datepicker-unselectable { - background-color: $calendar-unselectable-bg; + background-color: $gray-light; } } .dropdown-menu-inner-title { display: block; - color: $gl-title-color; + color: $gl-text-color; font-weight: 600; } .dropdown-menu-inner-content { display: block; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .dropdown-toggle-text { &.is-default { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/framework/emojis.scss similarity index 99% rename from app/assets/stylesheets/pages/emojis.scss rename to app/assets/stylesheets/framework/emojis.scss index f17797b23819706aa4b129e5e6afb253b9dbfe8f..7158de65143003079fef00eb00a77339c7a8230e 100644 --- a/app/assets/stylesheets/pages/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,4 +1,4 @@ -.emoji-0023-20E3 { background-position: 0 0px; } +.emoji-0023-20E3 { background-position: 0 0; } .emoji-002A-20E3 { background-position: -20px 0; } .emoji-0030-20E3 { background-position: 0 -20px; } .emoji-0031-20E3 { background-position: -20px -20px; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ab0b81f77f7569f3614a15489ce5fbc6b51abe0f..c51912b4ac46afd71b73f5416af67bd1473d0f94 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -19,7 +19,7 @@ .file-title { position: relative; - background-color: $background-color; + background-color: $gray-light; border-bottom: 1px solid $border-color; margin: 0; text-align: left; @@ -182,3 +182,52 @@ span.idiff { border-bottom-right-radius: 2px; } } + +.file-stats { + ul { + list-style: none; + margin: 0; + padding: 10px 0; + + li { + padding: 3px 0; + line-height: 20px; + } + } + + .new-file { + a { + color: $gl-text-green; + } + } + + .renamed-file { + a { + color: $gl-text-orange; + } + } + + .deleted-file { + a { + color: $gl-text-red; + } + } + + .edit-file { + a { + color: $gl-text-color; + } + } + + a { + text-decoration: none; + + .new-file { + color: $notify-new-file; + } + + .deleted-file { + color: $notify-deleted-file; + } + } +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 19827943385a10ca75db2362a9d2f14eb46cb6e2..fee38b0502391fb5993a469433090790bd752f45 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -23,3 +23,118 @@ } } +.filtered-search-container { + display: -webkit-flex; + display: flex; +} + +.filtered-search-input-container { + display: -webkit-flex; + display: flex; + position: relative; + width: 100%; + + .form-control { + padding-left: 25px; + padding-right: 25px; + + &:focus ~ .fa-filter { + color: $common-gray-dark; + } + } + + .fa-filter { + position: absolute; + top: 10px; + left: 10px; + color: $gray-darkest; + } + + .fa-times { + right: 10px; + color: $gray-darkest; + } + + .clear-search { + width: 35px; + background-color: transparent; + border: none; + position: absolute; + right: 0; + height: 100%; + outline: none; + + &:hover .fa-times { + color: $common-gray-dark; + } + } +} + +.dropdown-menu .filter-dropdown-item { + padding: 0; +} + +.filter-dropdown { + max-height: 215px; + overflow-x: scroll; +} + +.filter-dropdown-item { + .btn { + border: none; + width: 100%; + text-align: left; + padding: 8px 16px; + text-overflow: ellipsis; + overflow-y: hidden; + border-radius: 0; + + .fa { + width: 15px; + } + + .dropdown-label-box { + border-color: $white-light; + border-style: solid; + border-width: 1px; + width: 17px; + height: 17px; + } + + &:hover, + &:focus { + background-color: $dropdown-hover-color; + color: $white-light; + text-decoration: none; + + .avatar { + border-color: $white-light; + } + } + } + + .dropdown-light-content { + font-size: 14px; + font-weight: 400; + } + + .dropdown-user { + display: -webkit-flex; + display: flex; + } + + .dropdown-user-details { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + } +} + +.hint-dropdown { + width: 250px; +} + +.filter-dropdown-loading { + padding: 8px 16px; +} diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 25a2b38baaadf19bd244beee5c05f7b34aeeff20..25d6fbe465affaabf7ea83d6afbc049843c67556 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -22,7 +22,7 @@ input[type='text'].danger { margin-top: 0; margin-bottom: -$gl-padding; padding: $gl-padding; - background-color: $background-color; + background-color: $gray-light; border-top: 1px solid $border-color; } @@ -96,6 +96,10 @@ label { code { line-height: 1.8; } + + img { + margin-right: $gl-padding; + } } @media(max-width: $screen-xs-max) { @@ -149,7 +153,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .input-group { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 5cd242af91df45b6661b5e510ea8f5f4cebf92de..d6566dc4ec91f33dd48e1515fe7824e68a1b5677 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -80,29 +80,32 @@ } } + .about-gitlab { + color: $color-light; + } } } } -$theme-charcoal: #3d454d; -$theme-charcoal-light: #485157; -$theme-charcoal-dark: #383f45; -$theme-charcoal-text: #b9bbbe; +$theme-charcoal-light: #b9bbbe; +$theme-charcoal: #485157; +$theme-charcoal-dark: #3d454d; +$theme-charcoal-darker: #383f45; $theme-blue-light: #becde9; $theme-blue: #2980b9; $theme-blue-dark: #1970a9; $theme-blue-darker: #096099; -$theme-graphite-lighter: #ccc; -$theme-graphite-light: #777; -$theme-graphite: #666; -$theme-graphite-dark: #555; +$theme-graphite-light: #ccc; +$theme-graphite: #777; +$theme-graphite-dark: #666; +$theme-graphite-darker: #555; -$theme-gray-light: #979797; -$theme-gray: #373737; -$theme-gray-dark: #272727; -$theme-gray-darker: #222; +$theme-black-light: #979797; +$theme-black: #373737; +$theme-black-dark: #272727; +$theme-black-darker: #222; $theme-green-light: #adc; $theme-green: #019875; @@ -120,15 +123,15 @@ body { } &.ui_charcoal { - @include gitlab-theme($theme-charcoal-text, $theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark); + @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker); } &.ui_graphite { - @include gitlab-theme($theme-graphite-lighter, $theme-graphite-light, $theme-graphite, $theme-graphite-dark); + @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker); } - &.ui_gray { - @include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker); + &.ui_black { + @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker); } &.ui_green { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index cc2286038c01caf6236e4d6743119e434aeec21f..24a1ce2b84d7471ede7cec9b85f9c42233c0fe6d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -9,7 +9,7 @@ header { &.navbar-empty { height: $header-height; background: $white-light; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .center-logo { margin: 8px 0; @@ -27,7 +27,7 @@ header { z-index: 100; margin-bottom: 0; height: $header-height; - background-color: $background-color; + background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -45,7 +45,7 @@ header { padding: 0; .nav > li > a { - color: $gl-icon-color; + color: $gl-text-color-secondary; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -57,13 +57,21 @@ header { &.header-user-dropdown-toggle { margin-left: 14px; + + &:hover, + &:focus, + &:active { + .header-user-avatar { + border-color: rgba($avatar-border, .2); + } + } } &:hover, &:focus, &:active { - background-color: $background-color; - color: darken($gl-icon-color, 30%); + background-color: $gray-light; + color: darken($gl-text-color-secondary, 30%); .todos-pending-count { background: darken($todo-alert-blue, 10%); @@ -84,11 +92,11 @@ header { padding: 6px 10px; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; } &.active { - color: $gl-icon-color; + color: $gl-text-color-secondary; } } } @@ -100,10 +108,11 @@ header { font-size: 18px; padding: 6px 10px; border: none; - background-color: $background-color; + background-color: $gray-light; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; + color: $gl-header-nav-hover-color; } } } @@ -180,6 +189,7 @@ header { &:hover { text-decoration: underline; + color: $gl-header-nav-hover-color; } } @@ -198,7 +208,7 @@ header { cursor: pointer; &:hover { - color: darken($color: $gl-text-color, $amount: 30%); + color: $gl-header-nav-hover-color; } } @@ -271,4 +281,5 @@ header { float: left; margin-right: 5px; border-radius: 50%; + border: 1px solid $avatar-border; } diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/framework/icons.scss similarity index 80% rename from app/assets/stylesheets/pages/icons.scss rename to app/assets/stylesheets/framework/icons.scss index 226bd2ead31e5f20b6e9054119cc5d74f9f393d8..dccf5177e351338b5c6a5ca9714710878305ecbf 100644 --- a/app/assets/stylesheets/pages/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -34,10 +34,10 @@ .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found { - color: $gl-gray; + color: $gl-text-color; svg { - fill: $gl-gray; + fill: $gl-text-color; } } @@ -49,3 +49,11 @@ fill: $gray-darkest; } } + +.ci-status-icon-manual { + color: $gl-text-color; + + svg { + fill: $gl-text-color; + } +} diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 878f44116ba11ab6653740144ba853efd6f37b62..09a569ad415c45ee4960f95b571bbd6f40d5be83 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -4,7 +4,7 @@ } .appearance-light-logo-preview { - background-color: $background-color; + background-color: $gray-light; max-width: 72px; padding: 10px; margin-bottom: 10px; diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 44834a84234a7b39909e2561c0d38b09227c26fb..46632f15f35d76fea3d8f336ad15497bcacbad4c 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -41,6 +41,6 @@ } &.status-box-upcoming { - background: $issue-box-upcoming-bg; + background: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index dfaf2f7f1d369857b9eadc8822db627db85f2a7d..29d55c446992e3b5a11ac50aecc4f9784b64651d 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -26,6 +26,62 @@ body { .container-limited { max-width: $fixed-layout-width; + + &.limit-container-width { + max-width: $limited-layout-width; + } +} + +.alert-wrapper { + .alert { + margin-bottom: 0; + + &:last-child { + margin-bottom: $gl-padding; + } + } + + .alert-link-group { + float: right; + } + + /* Center alert text and alert action links on smaller screens */ + @media (max-width: $screen-sm-max) { + .alert { + text-align: center; + } + + .alert-link-group { + float: none; + } + } + + /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ + .alert-warning { + transition: background-color 0.15s, border-color 0.15s; + background-color: lighten($gl-warning, 4%); + border-color: lighten($gl-warning, 4%); + } + + .alert-warning + .alert-warning { + background-color: $gl-warning; + border-color: $gl-warning; + } + + .alert-warning + .alert-warning + .alert-warning { + background-color: darken($gl-warning, 4%); + border-color: darken($gl-warning, 4%); + } + + .alert-warning + .alert-warning + .alert-warning + .alert-warning { + background-color: darken($gl-warning, 8%); + border-color: darken($gl-warning, 8%); + } + + .alert-warning:only-of-type { + background-color: $gl-warning; + border-color: $gl-warning; + } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index ed4b60faf92c88cf58ff3fe508bb07d1a50b20dc..1c6698ad0c67c2e6e45abd559d8ff4bde80ed56b 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -36,7 +36,7 @@ color: $list-warning-row-color; } - &.smoke { background-color: $background-color; } + &.smoke { background-color: $gray-light; } &:not(.ui-sort-disabled):hover { background: $row-hover; @@ -46,7 +46,7 @@ border-bottom: none; &.bottom { - background: $background-color; + background: $gray-light; } } @@ -59,7 +59,7 @@ p { padding-top: 1px; margin: 0; - color: $gray-dark; + color: $white-normal; img { position: relative; @@ -113,7 +113,7 @@ ul.content-list { padding: 0; li { - border-color: $table-border-color; + border-color: $white-normal; font-size: $list-font-size; color: $list-text-color; @@ -128,7 +128,7 @@ ul.content-list { } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .member-group-link { @@ -163,6 +163,10 @@ ul.content-list { &:last-child { margin-right: 0; + + @media(max-width: $screen-xs-max) { + margin: 0 auto; + } } } @@ -186,7 +190,7 @@ ul.content-list { &.list-placeholder { background-color: $gray-light; - border: dotted 1px $gray-dark; + border: dotted 1px $white-normal; margin: 1px 0; min-height: 52px; } @@ -199,6 +203,7 @@ ul.content-list { display: -webkit-flex; display: -ms-flexbox; display: flex; + align-items: center; white-space: nowrap; } @@ -208,6 +213,11 @@ ul.content-list { padding-right: 8px; } + .row-fixed-content { + flex: 0 0 auto; + margin-left: auto; + } + .row-title { font-weight: 600; } @@ -224,22 +234,53 @@ ul.content-list { } .label-default { - color: $btn-transparent-color; + color: $gl-text-color-secondary; } } -.panel > .content-list > li { - padding: $gl-padding-top $gl-padding; +// Table list +.table-list { + display: table; + width: 100%; + + .table-list-row { + display: table-row; + } + + .table-list-cell { + display: table-cell; + vertical-align: top; + padding: 10px 16px; + border-bottom: 1px solid $gray-darker; + + &.avatar-cell { + width: 36px; + padding-right: 0; + + img { + margin-right: 0; + } + } + } + + &.table-wide { + .table-list-cell { + &:last-of-type { + padding-right: 0; + } - &.commit { - @media (min-width: $screen-sm-min) { - padding-left: 46px + $gl-padding; + &:first-of-type { + padding-left: 0; + } } } } +.panel > .content-list > li { + padding: $gl-padding-top $gl-padding; +} + ul.controls { - padding-top: 1px; float: right; list-style: none; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 59a30d31ac7d22c2ebaae20aa8c7d8b9de1a7956..5bff694658cd2a3666658707e65df5f2eb854968 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -73,7 +73,7 @@ } .referenced-users { - color: $gl-header-color; + color: $gl-text-color; padding-top: 10px; } @@ -114,7 +114,7 @@ // Border around images in issue and MR comments. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px 0; // Ensure that image does not exceed viewport @@ -135,7 +135,7 @@ .toolbar-btn { float: left; padding: 0 5px; - color: $note-toolbar-color; + color: $gl-text-color-secondary; background: transparent; border: 0; outline: 0; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 4f2ac77f228b2762ccf45163f7a239c0953b0485..1acd06122a3dde51ce2e9aceb1ef58b9b8c58975 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -46,7 +46,7 @@ &.light { a { - color: $gl-gray; + color: $gl-text-color; } } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index abfdd7a759d1e4bb0262aed86545b253bd69930c..8e2c56a84885f940bc868c7b65846336e63e89f6 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,21 +23,21 @@ margin-right: 0; } - .issues-details-filters, + .issues-details-filters:not(.filtered-search-block), .dash-projects-filters, .check-all-holder { display: none; } - .rss-btn { + .issues-holder .issue-check { display: none; } - .project-home-links { + .rss-btn { display: none; } - .project-avatar { + .project-home-links { display: none; } @@ -54,7 +54,7 @@ } // Display Star and Fork buttons without counters on mobile. - .project-action-buttons { + .project-repo-buttons { display: block; .count-buttons .btn { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ea77348633df5312fc15e0f877913d4dac0b2672..401c2d0f6ee6f1b2259cd144e76eaff9d03455ba 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -51,7 +51,7 @@ margin-bottom: -1px; font-size: 14px; line-height: 28px; - color: $note-toolbar-color; + color: $gl-text-color-secondary; border-bottom: 2px solid transparent; &:hover, @@ -76,21 +76,14 @@ color: $black; } } - - .badge { - font-weight: normal; - background-color: $nav-badge-bg; - color: $btn-transparent-color; - vertical-align: baseline; - } } &.sub-nav { text-align: center; - background-color: $dark-background-color; + background-color: $gray-normal; .container-fluid { - background-color: $dark-background-color; + background-color: $gray-normal; margin-bottom: 0; } @@ -108,7 +101,7 @@ &:hover, &:active, &:focus { - border-bottom: none; + border-color: transparent; } } } @@ -117,14 +110,14 @@ .top-area { @include clearfix; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .nav-text { padding-top: 16px; padding-bottom: 11px; display: inline-block; - width: 50%; line-height: 28px; + white-space: normal; /* Small devices (phones, tablets, 768px and lower) */ @media (max-width: $screen-xs-max) { @@ -165,30 +158,24 @@ } .nav-controls { - width: 50%; display: inline-block; float: right; text-align: right; padding: 11px 0; margin-bottom: 0; - > .dropdown { - margin-right: $gl-padding-top; - display: inline-block; - vertical-align: top; - - &:last-child { - margin-right: 0; - } - } - - > .btn { + > .btn, + > .btn-container, + > .dropdown, + > input, + > form { margin-right: $gl-padding-top; display: inline-block; vertical-align: top; &:last-child { margin-right: 0; + float: right; } } @@ -196,19 +183,21 @@ float: none; } - > form { - display: inline-block; - } - .icon-label { display: none; } - input { + .btn, + .dropdown, + .dropdown-toggle, + input, + form { height: 35px; + } + + input { display: inline-block; position: relative; - margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ @media (min-width: $screen-md-min) { width: 200px; } @@ -232,6 +221,7 @@ .btn, form, .dropdown, + .dropdown-toggle, .dropdown-menu-toggle, .form-control { margin: 0 0 10px; @@ -270,6 +260,10 @@ .nav-text, .nav-controls { width: auto; + + @media (max-width: $screen-xs-max) { + width: 100%; + } } } @@ -282,6 +276,10 @@ padding: 17px 0; } } + + pre { + width: 100%; + } } .layout-nav { @@ -289,7 +287,7 @@ top: $header-height; width: 100%; z-index: 11; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; text-align: center; @@ -317,7 +315,7 @@ .fa-caret-down { margin-left: 5px; - color: $gl-icon-color; + color: $gl-text-color-secondary; } .dropdown { @@ -352,7 +350,7 @@ } .fade-right { - @include fade(left, $background-color); + @include fade(left, $gray-light); right: -5px; .fa { @@ -361,7 +359,7 @@ } .fade-left { - @include fade(right, $background-color); + @include fade(right, $gray-light); left: -5px; .fa { @@ -372,7 +370,7 @@ &.sub-nav-scroll { .fade-right { - @include fade(left, $dark-background-color); + @include fade(left, $gray-normal); right: 0; .fa { @@ -381,7 +379,7 @@ } .fade-left { - @include fade(right, $dark-background-color); + @include fade(right, $gray-normal); left: 0; .fa { @@ -435,3 +433,40 @@ } } } + +@media (max-width: $screen-xs-max) { + .top-area { + flex-flow: row wrap; + + .nav-controls { + $controls-margin: $btn-xs-side-margin - 2px; + flex: 0 0 100%; + + &.controls-flex { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + padding: 0 0 $gl-padding-top; + } + + .controls-item, + .controls-item-full, + .controls-item:last-child { + flex: 1 1 35%; + display: block; + width: 100%; + margin: $controls-margin; + + .btn, + .dropdown { + margin: 0; + } + } + + .controls-item-full { + flex: 1 1 100%; + } + } + } +} diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 85c1385d5d93d507670475528f3027c5245a563c..4decee2c525c5a0fc660d7f450adb3703d73d144 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -14,7 +14,7 @@ .header-action-buttons { i { - color: $gl-icon-color; + color: $gl-text-color-secondary; font-size: 13px; margin-right: 3px; } @@ -42,7 +42,7 @@ .commit-committer-link, .commit-author-link { - color: $gl-gray; + color: $gl-text-color; font-weight: bold; } @@ -57,7 +57,6 @@ } .ci-status-link { - svg { position: relative; top: 2px; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 5ba0486177fa49bfbbe3053cf2e4cc748ddf692e..efe93724013edba08e6af39101574251c2291e75 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -18,6 +18,20 @@ margin-top: -2px; margin-left: 5px; } + + &.split { + display: flex; + align-items: center; + } + + .left { + flex: 1 1 auto; + } + + .right { + flex: 0 0 auto; + text-align: right; + } } .panel-body { @@ -34,3 +48,11 @@ line-height: inherit; } } + +.panel-default { + .table-list-row:last-child { + .table-list-cell { + border-bottom: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index fde1431b13e42147742d419636c977e514958dde..9ab17e67d4c542c6a8137c6af128c37b2344a7d0 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -39,7 +39,7 @@ } &:hover { - background-color: $gray-dark; + background-color: $white-normal; border-color: $border-white-normal; color: $gl-text-color; } @@ -108,7 +108,7 @@ border-color: $input-border; color: $gl-text-color; line-height: 15px; - background-color: $background-color; + background-color: $gray-light; background-image: none; .select2-search-choice-close { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 0aa609b8dd55b660e189a8e396b524a8a9bfb135..f0b03710c79c0b30868a99bc970b92a68def0e74 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -23,7 +23,7 @@ .sidebar-wrapper { z-index: 1000; - background: $background-color; + background: $gray-light; .nicescroll-rails-hr { // TODO: Figure out why nicescroll doesn't hide horizontal bar @@ -101,6 +101,17 @@ padding: 0 8px; border-radius: 6px; } + + .about-gitlab { + padding: 7px $gl-sidebar-padding; + font-size: $gl-font-size; + line-height: 24px; + display: block; + text-decoration: none; + font-weight: normal; + position: absolute; + bottom: 10px; + } } .sidebar-action-buttons { @@ -172,7 +183,9 @@ &.right-sidebar-expanded { .line-resolve-all-container { - display: none; + @media (min-width: $sidebar-breakpoint) { + display: none; + } } } } @@ -223,9 +236,13 @@ header.header-sidebar-pinned { @media (min-width: $screen-md-min) { padding-right: $gutter_width; - .merge-request-tabs-holder.affix { + &:not(.with-overlay) .merge-request-tabs-holder.affix { right: $gutter_width; } + + &.with-overlay .merge-request-tabs-holder.affix { + right: $sidebar_collapsed_width; + } } &.with-overlay { diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/framework/snippets.scss similarity index 77% rename from app/assets/stylesheets/pages/snippets.scss rename to app/assets/stylesheets/framework/snippets.scss index ff13b86acf0da338756afe9fc8b728b12aaf211c..5f7e1b17cc700a0eb7f22b7c4621b51f65e84fc2 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -22,16 +22,6 @@ .snippet-file-content { border-radius: 3px; - margin-bottom: $gl-padding; - - .btn-clipboard { - @extend .btn; - } -} - -.project-snippets .awards { - border-bottom: 1px solid $table-border-color; - padding-bottom: $gl-padding; } .snippet-header { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 5d0ca63ea08e853bb65689f8902d49c24723276b..6d9fa74a030f646acf7179d338197d7cce6564a7 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -31,7 +31,7 @@ table { } th { - background-color: $background-color; + background-color: $gray-light; font-weight: normal; border-bottom: none; @@ -41,7 +41,7 @@ table { } td { - border-color: $table-border-color; + border-color: $white-normal; } } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 875cded8b4e7adb0b091b9ef6bc8228508725487..ff185cd8767c985015c271952965b9d849729e88 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -6,8 +6,8 @@ .timeline-entry { padding: $gl-padding $gl-btn-padding 11px; - border-color: $table-border-color; - color: $gl-gray; + border-color: $white-normal; + color: $gl-text-color; border-bottom: 1px solid $border-white-light; &:target { @@ -32,7 +32,7 @@ .system-note { .note-text { - color: $gl-gray !important; + color: $gl-text-color !important; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 55bc325b858c35c251b211006cdda4135ce48771..12d56359d7d4d8a8b6379d9a8049276c6616d41d 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -91,14 +91,14 @@ // Labels .label { padding: 4px 5px; - font-size: 13px; + font-size: 12px; font-style: normal; font-weight: normal; display: inline-block; &.label-gray { background-color: $label-gray-bg; - color: $gl-gray; + color: $gl-text-color; text-shadow: none; } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index c731a8f222f8d0fbf076d88adddef081c980c05c..0fc89d5976acfdf8f3442c4057934dcab218851a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -65,11 +65,11 @@ $legend-color: $text-color; // //## -$pagination-color: $gl-gray; +$pagination-color: $gl-text-color; $pagination-bg: $white-light; $pagination-border: $border-color; -$pagination-hover-color: $gl-gray; +$pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; @@ -78,7 +78,7 @@ $pagination-active-bg: $white-light; $pagination-active-border: $border-color; $pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $background-color; +$pagination-disabled-bg: $gray-light; $pagination-disabled-border: $border-color; @@ -117,10 +117,13 @@ $alert-border-radius: 0; $panel-border-radius: 2px; $panel-default-text: $text-color; $panel-default-border: $border-color; -$panel-default-heading-bg: $background-color; -$panel-footer-bg: $background-color; +$panel-default-heading-bg: $gray-light; +$panel-footer-bg: $gray-light; $panel-inner-border: $border-color; +$badge-bg: $badge-bg; +$badge-color: $badge-color; + //== Wells // //## @@ -153,8 +156,8 @@ $nav-link-padding: 13px $gl-padding; //== Code // //## -$pre-bg: $background-color !default; -$pre-color: $gl-gray !default; +$pre-bg: $gray-light !default; +$pre-color: $gl-text-color !default; $pre-border-color: $border-color; -$table-bg-accent: $background-color; +$table-bg-accent: $gray-light; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index d906d26bba923941e3b5ab72ed15005cfa63e3bf..bd58a26f429dae4604d1b203835fcba8f5fd58dc 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -1,5 +1,5 @@ @mixin md-typography { - color: $md-text-color; + color: $gl-text-color; word-wrap: break-word; a { @@ -50,14 +50,14 @@ margin: 16px 0 10px; padding: 0 0 0.3em; border-bottom: 1px solid $white-dark; - color: $gl-gray-dark; + color: $gl-text-color; } h2 { font-size: 1.5em; font-weight: 600; margin: 16px 0 10px; - color: $gl-gray-dark; + color: $gl-text-color; } h3 { @@ -100,7 +100,7 @@ } p { - color: $gl-text-color-dark; + color: $gl-text-color; margin: 6px 0 0; } @@ -108,7 +108,7 @@ @extend .table; @extend .table-bordered; margin: 12px 0; - color: $gl-text-color-dark; + color: $gl-text-color; th { background: $label-gray-bg; @@ -230,7 +230,7 @@ h3, h4, h5, h6 { - color: $gl-title-color; + color: $gl-text-color; font-weight: 600; } @@ -292,7 +292,7 @@ h2, h3, h4 { small { - color: $gl-gray; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a1d5f6427f4365628604da107cb7b013c6224b3c..07cb669a46e16b9ac04c2ec034abf5469ab82b9b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px; $sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; -$gutter_inner_width: 258px; +$gutter_inner_width: 250px; $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; @@ -17,16 +17,16 @@ $darken-dark-factor: 10%; $darken-border-factor: 5%; $white-light: #fff; -$white-normal: darken($white-light, $darken-normal-factor); -$white-dark: darken($white-light, $darken-dark-factor); +$white-normal: #f0f0f0; +$white-dark: #eaeaea; $gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; -$gray-normal: darken($gray-light, $darken-normal-factor); +$gray-normal: #f5f5f5; $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; -$gray-darkest: #c9c9c9; +$gray-darkest: #c4c4c4; $green-light: #3cbd70; $green-normal: darken($green-light, $darken-normal-factor); @@ -55,11 +55,10 @@ $black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: darken($white-light, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor); -$border-white-dark: darken($white-dark, $darken-border-factor); $border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); -$border-gray-dark: darken($gray-dark, $darken-border-factor); +$border-gray-dark: darken($white-normal, $darken-border-factor); $border-green-extra-light: #9adb84; $border-green-light: darken($green-light, $darken-border-factor); @@ -78,9 +77,6 @@ $border-red-light: darken($red-light, $darken-border-factor); $border-red-normal: darken($red-normal, $darken-border-factor); $border-red-dark: darken($red-dark, $darken-border-factor); -$help-well-bg: $gray-light; -$help-well-border: #e5e5e5; - $warning-message-bg: #fbf2d9; $warning-message-color: #9e8e60; $warning-message-border: #f0e2bb; @@ -90,10 +86,7 @@ $warning-message-border: #f0e2bb; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #f0f0f0; -$background-color: $gray-light; -$dark-background-color: #f5f5f5; -$table-text-gray: #8f8f8f; +$sidebar-collapsed-icon-color: #999; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -103,31 +96,26 @@ $well-light-text-color: #5b6169; * Text */ $gl-font-size: 14px; -$gl-title-color: #333; -$gl-text-color: #5c5c5c; -$gl-text-color-dark: #5c5d5e; -$gl-text-color-light: #8c8c8c; +$gl-text-color: rgba(0, 0, 0, .85); +$gl-text-color-secondary: rgba(0, 0, 0, .55); +$gl-text-color-disabled: rgba(0, 0, 0, .35); $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; $gl-link-color: #3777b0; -$gl-diff-text-color: #555; -$gl-dark-link-color: #333; -$gl-placeholder-color: #8f8f8f; -$gl-icon-color: $gl-placeholder-color; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; -$gl-gray-light: $gl-placeholder-color; $gl-header-color: #4c4e54; +$gl-header-nav-hover-color: #434343; /* * Lists */ $list-font-size: $gl-font-size; -$list-title-color: $gl-title-color; +$list-title-color: $gl-text-color; $list-text-color: $gl-text-color; -$list-text-disabled-color: #888; +$list-text-disabled-color: $gl-text-color-disabled; $list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; @@ -138,15 +126,14 @@ $list-warning-row-color: #8a6d3b; /* * Markdown */ -$md-text-color: $gl-text-color; $md-link-color: $gl-link-color; $md-area-border: #ddd; /* * Code */ -$code_font_size: 13px; -$code_line_height: 1.5; +$code_font_size: 12px; +$code_line_height: 1.6; /* * Padding @@ -166,11 +153,11 @@ $row-hover-border: #b2d7ff; $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; +$limited-layout-width: 990px; +$gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; -$btn-transparent-color: #8f8f8f; $settings-icon-size: 18px; -$provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $active-item-blue: #4a8bee; @@ -179,9 +166,7 @@ $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; $issue-status-expired: #cea61b; -$issuable-sidebar-color: #999; -$issuable-avatar-hover-border: #999; -$issuable-clipboard-color: #999; +$issuable-sidebar-color: $gl-text-color-secondary; $show-aside-bg: #eee; $show-aside-color: #777; $show-aside-shadow: #ddd; @@ -193,7 +178,9 @@ $count-arrow-border: #dce0e5; $save-project-loader-color: #555; $divergence-graph-bar-bg: #ccc; $divergence-graph-separator-bg: #ccc; -$issue-box-upcoming-bg: #8f8f8f; +$general-hover-transition-duration: 150ms; +$general-hover-transition-curve: linear; + /* * Common component specific colors @@ -246,8 +233,6 @@ $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; -$match-line: $gray-light; -$table-border-gray: #f0f0f0; $line-target-blue: #f6faff; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; @@ -257,7 +242,6 @@ $file-mode-changed: #777; $file-mode-changed: #777; $diff-image-bg: #ddd; $diff-image-info-color: grey; -$diff-image-img-bg: #e5e5e5; $diff-swipe-border: #999; $diff-view-modes-color: grey; $diff-view-modes-border: #c1c1c1; @@ -272,14 +256,12 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; -$dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); $dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); -$dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; @@ -287,31 +269,31 @@ $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-chevron-size: 10px; +$dropdown-toggle-active-border-color: darken($border-color, 14%); + -$dropdown-toggle-bg: #fff; -$dropdown-toggle-color: #5c5c5c; -$dropdown-toggle-border-color: #e5e5e5; -$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%); -$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); -$dropdown-toggle-icon-color: #c4c4c4; -$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); +/* +* Filtered Search +*/ +$dropdown-hover-color: #3b86ff; /* * Buttons */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; -$btn-placeholder-gray: #c7c7c7; $btn-white-active: #848484; -$btn-gray-hover: #eee; + +/* +* Badges +*/ +$badge-bg: #eee; +$badge-color: $gl-text-color-secondary; /* * Award emoji */ -$award-emoji-menu-bg: #fff; -$award-emoji-menu-border: #f1f2f4; $award-emoji-menu-shadow: rgba(0,0,0,.175); -$award-emoji-new-btn-icon-color: #dcdcdc; /* * Search Box @@ -319,22 +301,15 @@ $award-emoji-new-btn-icon-color: #dcdcdc; $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; -$location-badge-color: #aaa; -$location-badge-bg: $dark-background-color; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; -$location-icon-active-color: #807e7e; /* * Notes */ -$notes-light-color: #8e8e8e; -$notes-action-color: #c3c3c3; -$notes-role-color: #8e8e8e; -$notes-role-border-color: #e4e4e4; +$notes-light-color: $gl-text-color-secondary; +$notes-role-color: $gl-text-color-secondary; $note-disabled-comment-color: #b2b2b2; -$note-form-border-color: #e5e5e5; -$note-toolbar-color: #959494; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; @@ -344,15 +319,12 @@ $note-line2-border: #ddd; * Zen */ $zen-control-color: #555; -$zen-control-hover-color: #111; /* * Calendar */ -$calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); -$calendar-unselectable-bg: $gray-light; $calendar-user-contrib-text: #959494; /* @@ -361,20 +333,13 @@ $calendar-user-contrib-text: #959494; $cycle-analytics-box-padding: 30px; $cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-big-font: 19px; -$cycle-analytics-dark-text: $gl-title-color; +$cycle-analytics-dark-text: $gl-text-color; $cycle-analytics-light-gray: #bfbfbf; $cycle-analytics-dismiss-icon-color: #b2b2b2; -/* - * Personal Access Tokens - */ -$personal-access-tokens-disabled-label-color: #bbb; - /* * CI */ -$ci-output-bg: #1d1f21; -$ci-text-color: #c5c8c6; $ci-skipped-color: #888; /* @@ -414,14 +379,13 @@ $callout-success-color: #3c763d; /* * Commit Page */ -$commit-committer-color: #999; $commit-max-width-marker-color: rgba(0, 0, 0, 0.0); $commit-message-text-area-bg: rgba(0, 0, 0, 0.0); /* * Common */ -$common-gray: $gl-gray; +$common-gray: $gl-text-color; $common-gray-light: #bbb; $common-gray-dark: #444; $common-red: $gl-text-red; @@ -470,9 +434,9 @@ $help-shortcut-header-color: #333; /* * Issues */ -$issues-border: #e5e5e5; $issues-today-bg: #f3fff2; $issues-today-border: #e1e8d5; +$compare-display-color: #888; /* * jQuery UI @@ -486,7 +450,7 @@ $jq-ui-default-color: #777; $label-gray-bg: #f8fafc; $label-inverse-bg: #333; $label-remove-border: rgba(0, 0, 0, .1); -$label-border-radius: 14px; +$label-border-radius: 100px; /* * Lint @@ -522,16 +486,15 @@ $project-option-descr-color: #54565b; $project-breadcrumb-color: #999; $project-private-forks-notice-odd: #2aa056; $project-network-controls-color: #888; -$project-limit-message-bg: #f28d35; /* * Runners */ $runner-state-shared-bg: #32b186; $runner-state-specific-bg: #3498db; -$runner-status-online-color: green; -$runner-status-offline-color: gray; -$runner-status-paused-color: red; +$runner-status-online-color: $green-normal; +$runner-status-offline-color: $gray-darkest; +$runner-status-paused-color: $red-normal; /* Stat Graph @@ -572,3 +535,10 @@ $body-text-shadow: rgba(255,255,255,0.01); */ $ui-dev-kit-example-color: #bbb; $ui-dev-kit-example-border: #ddd; + +/* +Pipeline Graph +*/ +$stage-hover-bg: #eaf3fc; +$stage-hover-border: #d1e7fc; +$action-icon-color: #d6d6d6; \ No newline at end of file diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index f2860dfe84d3194716136667429c7686085d7164..32eb750180f77977068e6485df80f925feaee7b1 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -1,6 +1,6 @@ .info-well { - background: $background-color; - color: $gl-gray; + background: $gray-light; + color: $gl-text-color; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -45,7 +45,7 @@ } .light-well { - background-color: $background-color; + background-color: $gray-light; padding: 15px; } diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index e5c7d70d45a098b971f4380e76b93ec1b1873e99..97ade638db6be1a8d9c9a6caf6ad0e0db8e905ec 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -40,7 +40,7 @@ } .zen-control-full { - color: $note-toolbar-color; + color: $gl-text-color-secondary; &:hover { color: $gl-link-color; @@ -57,6 +57,6 @@ font-size: 36px; &:hover { - color: $zen-control-hover-color; + color: $black; } } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 1adab3ffd94f1d82635a7b48622f938e4c19abfa..b425c78e0d56e01fddcf084b06bd7296881d93e6 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -3,7 +3,7 @@ /* * White Syntax Colors */ -$white-code-color: #333; +$white-code-color: $gl-text-color; $white-highlight: #fafe3d; $white-pre-hll-bg: #f8eec7; $white-hll-bg: #f8f8f8; @@ -69,14 +69,14 @@ $white-gc-bg: #eaf2f5; @mixin matchLine { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } .code.white { // Line numbers .line-numbers, .diff-line-num { - background-color: $background-color; + background-color: $gray-light; } .diff-line-num, @@ -87,7 +87,7 @@ $white-gc-bg: #eaf2f5; // Code itself pre.code, .diff-line-num { - border-color: $table-border-gray; + border-color: $white-normal; } &, diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 024b4df6bd09bcc28f8fe536cd5a714041dac715..60ff72c703e2309caf64da5f229b2084ee21828a 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -91,9 +91,9 @@ $highlighted-gc-bg: #eaf2f5; padding: 0 5px; text-align: right; width: 35px; - background-color: $background-color; + background-color: $gray-light; color: $black-transparent; - border-right: 1px solid $table-border-gray; + border-right: 1px solid $white-normal; &.old { background-color: $line-number-old; @@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5; &.match { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index ddc382362f7ce356355edab1a48f3ce6e19a180b..a81e5eb5ebf9fce6574e5717fddf65d159ace7af 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -18,15 +18,3 @@ p.details { pre.commit-message { white-space: pre-wrap; } - -.file-stats > a { - text-decoration: none; - - > .new-file { - color: $notify-new-file; - } - - > .deleted-file { - color: $notify-deleted-file; - } -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0d9cf679e7c6459b3b65cf9b50440d0ebaf55016..f2d60bff2b50e441cde0a7740469679ec9ddb0cd 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -31,7 +31,7 @@ .dropdown-content { max-height: 150px; - } + } } .issue-board-dropdown-content { @@ -74,6 +74,7 @@ height: 475px; // Needed for PhantomJS height: calc(100vh - 220px); min-height: 475px; + transition: width .2s; &.is-compact { width: calc(100% - 290px); @@ -98,7 +99,7 @@ .board-inner { height: 100%; font-size: $issue-boards-font-size; - background: $background-color; + background: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; } @@ -109,6 +110,12 @@ &.has-border { border-top: 3px solid; + margin-top: -1px; + margin-right: -1px; + margin-left: -1px; + padding-top: 1px; + padding-right: 1px; + padding-left: 1px; .board-title { padding-top: ($gl-padding - 3px); @@ -253,7 +260,7 @@ .board-list-count { padding: 10px 0; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; font-size: 13px; > .fa { @@ -332,3 +339,18 @@ } } } + +.right-sidebar.right-sidebar-expanded { + &.boards-sidebar-slide-enter-active, + &.boards-sidebar-slide-leave-active { + transition: width .2s, + padding .2s; + } + + &.boards-sidebar-slide-enter, + &.boards-sidebar-slide-leave-active { + width: 0; + padding-left: 0; + padding-right: 0; + } +} diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss new file mode 100644 index 0000000000000000000000000000000000000000..3e2fa8ca88d7eea3547e463a8f3712f58e4f6a4a --- /dev/null +++ b/app/assets/stylesheets/pages/branches.scss @@ -0,0 +1,55 @@ +.divergence-graph { + padding: 12px 12px 0 0; + float: right; + + .graph-side { + position: relative; + width: 80px; + height: 22px; + padding: 5px 0 13px; + float: left; + + .bar { + position: absolute; + height: 4px; + background-color: $divergence-graph-bar-bg; + } + + .bar-behind { + right: 0; + border-radius: 3px 0 0 3px; + } + + .bar-ahead { + left: 0; + border-radius: 0 3px 3px 0; + } + + .count { + padding-top: 6px; + padding-bottom: 0; + font-size: 12px; + color: $gl-text-color; + display: block; + } + + .count-behind { + padding-right: 4px; + text-align: right; + } + + .count-ahead { + padding-left: 4px; + text-align: left; + } + } + + .graph-separator { + position: relative; + width: 1px; + height: 18px; + margin: 5px 0 0; + float: left; + background-color: $divergence-graph-separator-bg; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index dcc13f6d74abe72a3c5d8261000fb9d4e50a0dbe..fd101d43b5bad87d73be6fa4acb36788ec221c02 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -1,3 +1,34 @@ +@keyframes fade-out-status { + 0%, 50% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes blinking-dots { + 0% { + background-color: rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,0.2); + } + + 25% { + background-color: rgba($white-light, 0.4); + box-shadow: 12px 0 0 0 rgba($white-light,2), + 24px 0 0 0 rgba($white-light,0.2); + } + + 75% { + background-color: rgba($white-light, 0.4); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,1); + } + + 100% { + background-color: rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,0.2); + } +} + .build-page { pre.trace { background: $builds-trace-bg; @@ -14,45 +45,99 @@ } } - .scroll-controls { - .scroll-step { - width: 31px; - margin: 0 0 0 auto; + .environment-information { + background-color: $gray-light; + border: 1px solid $border-color; + padding: 12px $gl-padding; + border-radius: $border-radius-default; + + svg { + position: relative; + top: 1px; + margin-right: 5px; + } + } +} + +.scroll-controls { + height: 100%; + + .scroll-step { + width: 31px; + margin: 0 0 0 auto; + } + + .scroll-link, + .autoscroll-container { + right: 25px; + z-index: 1; + } + + .scroll-link { + position: fixed; + display: block; + margin-bottom: 10px; + + &.scroll-top .gitlab-icon-scroll-up-hover, + &.scroll-top:hover .gitlab-icon-scroll-up, + &.scroll-bottom .gitlab-icon-scroll-down-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down { + display: none; } - &.affix-bottom { - position: absolute; - right: 25px; + &.scroll-top:hover .gitlab-icon-scroll-up-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down-hover { + display: inline-block; } - &.affix { - right: 25px; - bottom: 15px; - z-index: 1; + &.scroll-top { + top: 110px; } - &.sidebar-expanded { - right: #{$gutter_width + ($gl-padding * 2)}; + &.scroll-bottom { + bottom: -2px; } + } - a { - display: block; - margin-bottom: 10px; + .autoscroll-container { + position: absolute; + } + + &.sidebar-expanded { + + .scroll-link, + .autoscroll-container { + right: ($gutter_width + ($gl-padding * 2)); } } +} - .environment-information { - background-color: $background-color; - border: 1px solid $border-color; - padding: 12px $gl-padding; - border-radius: $border-radius-default; +.status-message { + display: inline-block; + color: $white-light; - svg { - position: relative; - top: 1px; - margin-right: 5px; + .status-icon { + display: inline-block; + width: 16px; + height: 33px; + } + + .status-text { + float: left; + opacity: 0; + margin-right: 10px; + font-weight: normal; + line-height: 1.8; + transition: opacity 1s ease-out; + + &.animate { + animation: fade-out-status 2s ease; } } + + &:hover .status-text { + opacity: 1; + } } .build-header { @@ -75,7 +160,7 @@ flex: 1; a { - color: $gl-gray; + color: $gl-text-color; &:hover { color: $gl-link-color; @@ -96,8 +181,8 @@ } .build-trace { - background: $ci-output-bg; - color: $ci-text-color; + background: $black; + color: $gray-darkest; white-space: pre; overflow-x: auto; font-size: 12px; @@ -109,6 +194,15 @@ .bash { display: block; } + + .build-loader-animation { + position: relative; + width: 6px; + height: 6px; + margin: auto auto 12px 2px; + border-radius: 50%; + animation: blinking-dots 1s linear infinite; + } } .right-sidebar.build-sidebar { @@ -248,6 +342,12 @@ } } +.build-sidebar { + .container-fluid.container-limited { + max-width: 100%; + } +} + .build-detail-row { margin-bottom: 5px; @@ -257,7 +357,7 @@ } .build-light-text { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index d1cd1e5d848790cfc195f716dd1be4dbf6f29409..9064383239097e0f2cec119d2817f64c24c9ab72 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -18,7 +18,7 @@ } td { - color: $gl-gray; + color: $gl-text-color; vertical-align: middle !important; a { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss deleted file mode 100644 index bf656d0e28ee1f3e8f086d3cfee1ed483b2932f9..0000000000000000000000000000000000000000 --- a/app/assets/stylesheets/pages/commit.scss +++ /dev/null @@ -1,132 +0,0 @@ -.commit-title { - display: block; -} - -.commit-author, -.commit-committer { - display: block; - color: $commit-committer-color; - font-weight: normal; - font-style: italic; -} - -.commit-author strong, -.commit-committer strong { - font-weight: bold; - font-style: normal; -} - -.commit-description { - background: none; - border: none; - margin: 0; - padding: 0; - margin-top: 10px; - word-break: normal; - white-space: pre-wrap; -} - -.js-details-expand { - &:hover { - text-decoration: none; - } -} - -.ci-status-link { - svg { - overflow: visible; - } -} - -.commit-box { - border-top: 1px solid $border-color; - padding: $gl-padding 0; - - .commit-title { - margin: 0; - font-size: 23px; - color: $gl-gray-dark; - } - - .commit-description { - margin-top: 15px; - } -} - -.commit-hash-full { - @media (max-width: $screen-sm-max) { - width: 80px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: bottom; - } -} - -.file-stats { - ul { - list-style: none; - margin: 0; - padding: 10px 0; - - li { - padding: 3px 0; - line-height: 20px; - } - } - - .new-file { - a { - color: $gl-text-green; - } - } - - .renamed-file { - a { - color: $gl-text-orange; - } - } - - .deleted-file { - a { - color: $gl-text-red; - } - } - - .edit-file { - a { - color: $gl-text-color; - } - } -} - -/* - * Commit message textarea for web editor and - * custom merge request message - */ -.commit-message-container { - background-color: $body-bg; - position: relative; - font-family: $monospace_font; - $left: 12px; - overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 - .max-width-marker { - width: 72ch; - color: $commit-max-width-marker-color; - font-family: inherit; - left: $left; - height: 100%; - border-right: 1px solid mix($input-border, $white-light); - position: absolute; - z-index: 1; - } - - > textarea { - background-color: $commit-message-text-area-bg; - font-family: inherit; - padding-left: $left; - position: relative; - z-index: 2; - } -} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c29b5fdea78a33dfe067fbd7da48f6be17a0f306..fef8e8eec27c14bdeb46dc618a9c34f9809ed6d0 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,14 +1,82 @@ +.commit-description { + background: none; + border: none; + padding: 0; + margin-top: 10px; + word-break: normal; + white-space: pre-wrap; +} + +.js-details-expand { + &:hover { + text-decoration: none; + } +} + +.commit-box { + border-top: 1px solid $border-color; + padding: $gl-padding 0; + + .commit-title { + margin: 0; + color: $gl-text-color; + } + + .commit-description { + margin-top: 15px; + } +} + +.commit-hash-full { + @media (max-width: $screen-sm-max) { + width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } +} + +/* + * Commit message textarea for web editor and + * custom merge request message + */ +.commit-message-container { + background-color: $body-bg; + position: relative; + font-family: $monospace_font; + $left: 12px; + overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 + .max-width-marker { + width: 72ch; + color: $commit-max-width-marker-color; + font-family: inherit; + left: $left; + height: 100%; + border-right: 1px solid mix($input-border, $white-light); + position: absolute; + z-index: 1; + } + + textarea { + background-color: $commit-message-text-area-bg; + font-family: inherit; + padding-left: $left; + position: relative; + z-index: 2; + } +} + + .commits-compare-switch { - @include btn-default; - @include btn-white; float: left; margin-right: 9px; } .commit-header { padding: 5px 10px; - background-color: $background-color; - border-top: 1px solid $gray-darker; + background-color: $gray-light; border-bottom: 1px solid $gray-darker; font-size: 14px; @@ -18,8 +86,6 @@ } .commit-row-title { - line-height: 1.35; - .notes_count { float: right; margin-right: 10px; @@ -30,15 +96,14 @@ } .commit-row-message { - color: $gl-dark-link-color; + color: $gl-text-color; } - } .text-expander { display: inline-block; background: $gray-light; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; padding: 0 5px; cursor: pointer; border: 1px solid $border-gray-dark; @@ -54,9 +119,8 @@ .commit-actions { @media (min-width: $screen-sm-min) { - float: right; - margin-left: $gl-padding; - margin-top: 2px; + width: 300px; + text-align: right; font-size: 0; } @@ -86,38 +150,16 @@ .commit, .generic_commit_status { - padding: 10px 0; - position: relative; - - @media (min-width: $screen-sm-min) { - padding-left: 46px; - } - - &:not(:last-child) { - border-bottom: 1px solid $gray-darker; - } a, button { - color: $gl-dark-link-color; + color: $gl-text-color; vertical-align: baseline; } - .avatar { - margin-left: -46px; - } - - .item-title { - display: inline-block; - - @media (min-width: $screen-sm-min) { - max-width: 70%; - } - } - .commit-row-description { font-size: 14px; - border-left: 1px solid $btn-gray-hover; + border-left: 1px solid $white-normal; padding: 10px 15px; margin: 10px 0; background: $gray-light; @@ -134,20 +176,7 @@ } a { - color: $gl-dark-link-color; - } - } - - .commit-row-info { - color: $gl-gray; - line-height: 1.35; - - a { - color: $gl-gray; - } - - .avatar { - margin-right: 8px; + color: $gl-text-color; } } @@ -164,7 +193,7 @@ } .branch-commit { - color: $gl-gray; + color: $gl-text-color; .commit-icon { text-align: center; @@ -174,7 +203,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-text-color-secondary; } } @@ -183,62 +212,6 @@ } .commit-row-message { - color: $gl-gray; - } -} - -.divergence-graph { - padding: 12px 12px 0 0; - float: right; - - .graph-side { - position: relative; - width: 80px; - height: 22px; - padding: 5px 0 13px; - float: left; - - .bar { - position: absolute; - height: 4px; - background-color: $divergence-graph-bar-bg; - } - - .bar-behind { - right: 0; - border-radius: 3px 0 0 3px; - } - - .bar-ahead { - left: 0; - border-radius: 0 3px 3px 0; - } - - .count { - padding-top: 6px; - padding-bottom: 0; - font-size: 12px; - color: $gl-title-color; - display: block; - } - - .count-behind { - padding-right: 4px; - text-align: right; - } - - .count-ahead { - padding-left: 4px; - text-align: left; - } - } - - .graph-separator { - position: relative; - width: 1px; - height: 18px; - margin: 5px 0 0; - float: left; - background-color: $divergence-graph-separator-bg; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 57146e1fccdc4d0810eb0db4733dc685aaf869c4..cda069e6c0ef97371fb28c725b88f2ae863e154d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -20,15 +20,19 @@ .fa { color: $cycle-analytics-light-gray; + + &:hover { + color: $gl-text-color; + } } .stage-header { - width: 28%; + width: 26%; padding-left: $gl-padding; } .median-header { - width: 12%; + width: 14%; } .event-header { @@ -111,14 +115,14 @@ line-height: 19px; font-size: 14px; font-weight: 600; - color: $gl-title-color; + color: $gl-text-color; } &.text { color: $layout-link-gray; &.value-col { - color: $gl-title-color; + color: $gl-text-color; } } } @@ -141,7 +145,7 @@ .dismiss-icon { position: absolute; - right: $cycle-analytics-dismiss-icon-color; + right: $cycle-analytics-box-padding; cursor: pointer; color: $cycle-analytics-dismiss-icon-color; } @@ -215,7 +219,6 @@ border-bottom: 1px solid transparent; border-right: 1px solid $border-color; background-color: $gray-light; - cursor: default; &.active { background-color: transparent; @@ -232,6 +235,7 @@ &:hover:not(.active) { background-color: $gray-lightest; box-shadow: inset 2px 0 0 0 $border-color; + cursor: pointer; } &:first-child { @@ -246,11 +250,11 @@ float: left; &.stage-name { - width: 70%; + width: 65%; } &.stage-median { - width: 30%; + width: 35%; } } @@ -260,7 +264,7 @@ .stage-empty, .not-available { - color: $gl-text-color-light; + color: $gl-text-color-secondary; } } } @@ -327,7 +331,7 @@ @include text-overflow(); a { - color: $gl-dark-link-color; + color: $gl-text-color; } } } @@ -355,7 +359,7 @@ .issue-link, .commit-author-link, .issue-author-link { - color: $gl-dark-link-color; + color: $gl-text-color; } // Custom CSS for components @@ -396,11 +400,11 @@ } .item-build-name { - color: $gl-title-color; + color: $gl-text-color; } .pipeline-id { - color: $gl-title-color; + color: $gl-text-color; padding: 0 3px 0 0; } @@ -423,7 +427,7 @@ } .fa { - color: $gl-text-color-light; + color: $gl-text-color-secondary; font-size: $code_font_size; } } @@ -435,7 +439,7 @@ width: 75%; margin: 0 auto; padding-top: 130px; - color: $gl-text-color-light; + color: $gl-text-color-secondary; h4 { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss new file mode 100644 index 0000000000000000000000000000000000000000..2fafe052106627f0383e29c77d89fec36c8812f0 --- /dev/null +++ b/app/assets/stylesheets/pages/deploy_keys.scss @@ -0,0 +1,13 @@ +.deploy-keys-list { + width: 100%; + overflow: auto; + + table { + border: 1px solid $table-border-color; + } +} + +.deploy-keys-title { + padding-bottom: 2px; + line-height: 2; +} diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 80baebd5ea37707f7b02cbc9e1c0a27da099de79..46fd19c93f9d275f4614473277d3f50a88194d4c 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,16 +1,15 @@ .detail-page-header { padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; - color: $gl-text-color-dark; - font-size: 16px; + color: $gl-text-color; line-height: 34px; .author { - color: $gl-text-color-dark; + color: $gl-text-color; } .identifier { - color: $gl-text-color-dark; + color: $gl-text-color; } .issue_created_ago, @@ -23,7 +22,7 @@ .title { margin: 0 0 16px; font-size: 2em; - color: $gl-gray-dark; + color: $gl-text-color; padding: 0 0 0.3em; border-bottom: 1px solid $white-dark; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 737f6e0f4bed212e0ba9dffe8af1904147c03269..96ba7c406344fd40f2d277ab93a6ba142281b95c 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -11,10 +11,10 @@ .diff-header { position: relative; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; padding: 10px 16px; - color: $gl-diff-text-color; + color: $gl-text-color; z-index: 10; border-radius: 3px 3px 0 0; @@ -38,7 +38,7 @@ cursor: pointer; &:hover { - background-color: $dark-background-color; + background-color: $gray-normal; } .diff-toggle-caret { @@ -50,7 +50,7 @@ overflow: auto; overflow-y: hidden; background: $white-light; - color: $gl-title-color; + color: $gl-text-color; border-radius: 0 0 3px 3px; .unfold { @@ -187,8 +187,8 @@ img { border: 1px solid $white-light; - background-image: linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%), - linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%); + background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), + linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; @@ -380,7 +380,7 @@ } cursor: default; - color: $gl-title-color; + color: $gl-text-color; } &.disabled { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 6cde9c592de67b9f04b6ee3f2d591cc9ba50f4bc..4af267403d8841c77af6b1a5bc55d778be399d18 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -10,7 +10,7 @@ } .ace_gutter-cell { - background-color: $background-color; + background-color: $gray-light; } .cancel-btn { @@ -34,7 +34,7 @@ } .editor-ref { - background: $background-color; + background: $gray-light; padding-right: $gl-padding; border-right: 1px solid $border-color; display: block; @@ -51,8 +51,16 @@ .new-file-name { display: inline-block; - width: 450px; + max-width: 450px; float: left; + + @media(max-width: $screen-md-max) { + width: 280px; + } + + @media(max-width: $screen-sm-max) { + width: 180px; + } } .file-buttons { @@ -67,7 +75,8 @@ .soft-wrap-toggle, .license-selector, .gitignore-selector, - .gitlab-ci-yml-selector { + .gitlab-ci-yml-selector, + .dockerfile-selector { display: inline-block; vertical-align: top; font-family: $regular_font; @@ -97,7 +106,8 @@ .gitignore-selector, .license-selector, - .gitlab-ci-yml-selector { + .gitlab-ci-yml-selector, + .dockerfile-selector { .dropdown { line-height: 21px; } @@ -114,3 +124,42 @@ } } } + +@media(max-width: $screen-xs-max){ + .file-editor { + .file-title { + .pull-right { + height: auto; + } + } + + .new-file-name { + max-width: none; + width: 100%; + margin-bottom: 3px; + } + + .file-buttons { + display: block; + width: 100%; + margin-bottom: 10px; + + .soft-wrap-toggle { + width: 100%; + margin: 3px 0; + } + + .encoding-selector, + .license-selector, + .gitignore-selector, + .gitlab-ci-yml-selector { + display: block; + margin: 3px 0; + + button { + width: 100%; + } + } + } + } +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e716f24c8e295ace650af5940abbb5c9189e7446..778ef01430e6a74c2291b951586f2e63aa453b31 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -30,19 +30,25 @@ display: table-cell; } + .environments-name, .environments-commit, .environments-actions { width: 20%; } - .environments-deploy, - .environments-build, .environments-date { width: 10%; } - .environments-name { - width: 30%; + .environments-deploy, + .environments-build { + width: 15%; + } + + .environment-name, + .environments-build-cell, + .deployment-column { + word-break: break-all; } .deployment-column { @@ -66,25 +72,25 @@ .external-url, .dropdown-new { - color: $table-text-gray; + color: $gl-text-color-secondary; } .dropdown-menu { .fa { margin-right: 6px; - color: $table-text-gray; + color: $gl-text-color-secondary; } } .build-link, .branch-name { - color: $gl-dark-link-color; + color: $gl-text-color; } .stop-env-link, .external-url { - color: $table-text-gray; + color: $gl-text-color-secondary; .stop-env-icon { font-size: 14px; @@ -95,7 +101,7 @@ .build-column { .build-link { - color: $gl-dark-link-color; + color: $gl-text-color; } .avatar { @@ -115,13 +121,6 @@ .folder-name { cursor: pointer; - - .badge { - font-weight: normal; - background-color: $gray-darker; - color: $gl-placeholder-color; - vertical-align: baseline; - } } } @@ -136,4 +135,4 @@ margin-right: 0; } } -} +} \ No newline at end of file diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index dc67d411c7197277cd4ed86df30575f5871daad9..b989d72ce1c44198555f6d0e5fd718c69c80cbbb 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -5,7 +5,7 @@ .event-item { font-size: $gl-font-size; padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); - border-bottom: 1px solid $table-border-color; + border-bottom: 1px solid $white-normal; color: $list-text-color; &.event-inline { @@ -21,7 +21,7 @@ } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .avatar { diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss deleted file mode 100644 index 9b92128624c1053661040c1b2c6f2abc4d295f2c..0000000000000000000000000000000000000000 --- a/app/assets/stylesheets/pages/explore.scss +++ /dev/null @@ -1,8 +0,0 @@ -.explore-title { - text-align: center; - - h3 { - font-weight: normal; - font-size: 30px; - } -} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index a9af7af59e23b98e93e65fcb51a0c5a23f48d56d..d377526e655e820a47259b76a6ed6bb1fe2d29d3 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -13,7 +13,7 @@ .stats { float: right; line-height: $list-text-height; - color: $gl-gray; + color: $gl-text-color; span { margin-right: 15px; @@ -27,12 +27,6 @@ } } -.group-buttons { - .notification-dropdown { - display: inline-block; - } -} - .groups-header { @media (min-width: $screen-sm-min) { .nav-links { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index e2e644dc23bfb4bf6fc7176574d8c9079f9daf25..dae8ccdef6ca623efdd72cbf337fd390c5a0b722 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -60,7 +60,7 @@ // Border around images in the help pages. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px; max-height: calc(100vh - 100px); diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 407c0afbac8d4e66270ce3bf45e8db54ed032879..324c6cec96aa2866f64a6426c065f9af92532d1e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,3 +1,52 @@ +// Limit MR description for side-by-side diff view +.fixed-width-container { + max-width: $limited-layout-width - ($gl-padding * 2); + margin-left: auto; + margin-right: auto; +} + +.limit-container-width { + .detail-page-header { + @extend .fixed-width-container; + } + + .issuable-details { + .detail-page-description, + .mr-source-target, + .mr-state-widget, + .merge-manually { + @extend .fixed-width-container; + } + + .merge-request-tabs-holder { + &.affix { + border-bottom: 1px solid $border-color; + + .nav-links { + border: 0; + } + } + + .container-fluid { + @extend .fixed-width-container; + } + } + } + + .merge-request-details { + .emoji-list-container { + @extend .fixed-width-container; + } + } + + .diffs { + .mr-version-controls, + .files-changed { + @extend .fixed-width-container; + } + } +} + .issuable-details { section { .issuable-discussion { @@ -5,11 +54,16 @@ } } + .title { + padding: 0; + margin: 0; + border-bottom: none; + } + // Border around images in issue and MR descriptions. .description img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; - margin: 5px; max-height: calc(100vh - 100px); } } @@ -51,7 +105,7 @@ .block { @include clearfix; padding: $gl-padding 0; - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; // This prevents the mess when resizing the sidebar // of elements repositioning themselves.. width: $gutter_inner_width; @@ -98,10 +152,10 @@ } .edit-link { - color: $gl-gray; + color: $gl-text-color; - &:hover { - color: $md-link-color; + &:not([href]):hover { + color: rgba($avatar-border, .2); } } } @@ -134,7 +188,7 @@ } .btn-clipboard:hover { - color: $gl-gray; + color: $gl-text-color; } } @@ -169,7 +223,7 @@ } .no-value { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .sidebar-collapsed-icon { @@ -178,7 +232,7 @@ .gutter-toggle { margin-top: 7px; - border-left: 1px solid $border-gray-light; + border-left: 1px solid $border-gray-normal; } .assignee .avatar { @@ -216,7 +270,7 @@ } .participants { - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; } .hide-collapsed { @@ -237,7 +291,7 @@ color: $issuable-sidebar-color; &:hover { - color: $gl-gray; + color: $gl-text-color; } span { @@ -250,16 +304,16 @@ } .avatar:hover { - border-color: $issuable-avatar-hover-border; + border-color: $issuable-sidebar-color; } .btn-clipboard { border: none; - color: $issuable-clipboard-color; + color: $issuable-sidebar-color; &:hover { background: transparent; - color: $gl-gray; + color: $gl-text-color; } } } @@ -278,6 +332,10 @@ &:hover { color: $md-link-color; text-decoration: none; + + .avatar { + border-color: rgba($avatar-border, .2); + } } } @@ -333,7 +391,7 @@ margin-left: 5px; a { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } } @@ -388,6 +446,7 @@ .issuable-meta { display: inline-block; line-height: 18px; + font-size: 14px; } .js-issuable-selector-wrap { @@ -414,3 +473,102 @@ } } } + +.time_tracker { + padding-bottom: 0; + border-bottom: 0; + + + .sidebar-collapsed-icon { + + > .stopwatch-svg { + display: inline-block; + } + + svg { + width: 16px; + height: 16px; + fill: $sidebar-collapsed-icon-color; + } + + &:hover svg { + fill: $gl-text-color; + } + } + + .help-button, + .close-help-button { + cursor: pointer; + } + + .compare-meter { + &.within_estimate { + .meter-fill { + background: $gl-primary; + } + } + + &.over_estimate { + .meter-fill { + background: $red-light; + } + + .time-remaining, + .compare-value.spent { + color: $red-light; + } + } + } + + .meter-container { + background: $border-gray-light; + border-radius: 3px; + + .meter-fill { + max-width: 100%; + height: 5px; + border-radius: 3px; + background: $gl-primary; + } + } + + .compare-display-container { + display: flex; + justify-content: space-between; + margin-top: 5px; + + .compare-display { + font-size: 13px; + color: $compare-display-color; + + .compare-value { + color: $gl-text-color; + } + } + } + + .time-tracking-help-state { + background: $white-light; + margin: 16px -20px 0; + padding: 16px 20px; + border-top: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-light; + + a:hover { + color: $btn-white-active; + } + } + + .help-state-toggle-enter-active { + transition: all .8s ease; + } + + .help-state-toggle-leave-active { + transition: all .5s ease; + } + + .help-state-toggle-enter, + .help-state-toggle-leave-active { + opacity: 0; + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3b47f99df2c5db9450c890520cfc478f4c0b2880..8734a3b159838cc1fa0ed1ed87f4f24b10abc31c 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -88,12 +88,12 @@ ul.related-merge-requests > li { &.closed { background: $gray-light; - border-color: $issues-border; + border-color: $border-color; } &.merged { background: $gray-light; - border-color: $issues-border; + border-color: $border-color; } } @@ -144,7 +144,7 @@ ul.related-merge-requests > li { } .btn { - background-color: $background-color; - border: 1px solid $border-gray-light; + background-color: $gray-light; + border: 1px solid $border-gray-normal; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 25c91203ff48808ec8d677f749c4f45d168766ab..21d9b4c54ea9c783b360e17eef911066303d54a8 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -98,7 +98,7 @@ } .label { - padding: 9px; + padding: 8px 9px 9px; font-size: 14px; } } @@ -117,7 +117,7 @@ .manage-labels-list { .btn-action { - color: $gl-dark-link-color; + color: $gl-text-color; .fa { font-size: 18px; @@ -201,6 +201,12 @@ .label-remove { border-left: 1px solid $label-remove-border; z-index: 3; + border-radius: $label-border-radius; + padding: 6px 10px 6px 9px; + + &:hover { + box-shadow: inset 0 0 0 80px $label-remove-border; + } } .btn { diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index a7c80dce424b17ff3ffa6983254d0f6f86271409..68b6c5ecbd43bd2c6ebc1d26e91337d80a64e858 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -9,3 +9,13 @@ color: $lint-correct-color; } } + +.ci-linter { + .ci-editor { + height: 400px; + } + + .ci-template pre { + white-space: pre-wrap; + } +} diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index dd27a06fcd2f344ca224dd75f4c11fbf0cadce82..71ed5b1361a0e9ec657e7fae39512ebb3f0c95f0 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -17,14 +17,19 @@ line-height: 1.5; p { - font-size: 18px; + font-size: 16px; color: $login-brand-holder-color; } h1:first-child { font-weight: normal; - margin-bottom: 30px; + margin-bottom: 0.68em; margin-top: 0; + font-size: 34px; + } + + h3 { + font-size: 22px; } img { @@ -105,19 +110,19 @@ li { flex: 1; text-align: center; + border-left: 1px solid $border-color; &:first-of-type { + border-left: none; border-top-left-radius: $border-radius-default; } &:last-of-type { - border-left: 1px solid $border-color; border-top-right-radius: $border-radius-default; } &:not(.active) { background-color: $gray-light; - border-left: 1px solid $border-color; } a { diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 5f3bbb40ba08a6e8c9dc516632d6843cdf88c1da..be7193bae04aa93c5396381e371c89e67d845500 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -25,7 +25,7 @@ } .form-horizontal { - margin-top: 5px; + margin-top: 20px; @media (min-width: $screen-sm-min) { display: -webkit-flex; @@ -78,19 +78,48 @@ float: right; } + .dropdown { + width: 100%; + margin-top: 5px; + + .dropdown-menu-toggle { + vertical-align: middle; + width: 100%; + } + + @media (min-width: $screen-sm-min) { + margin-top: 0; + width: 155px; + } + } + .form-control { width: 100%; padding-right: 35px; @media (min-width: $screen-sm-min) { + width: 250px; + } + + @media (min-width: $screen-md-min) { width: 350px; } + + &.input-short { + @media (min-width: $screen-md-min) { + width: 170px; + } + + @media (min-width: $screen-lg-min) { + width: 210px; + } + } } } .member-search-btn { position: absolute; - right: 0; + right: 4px; top: 0; height: 35px; padding-left: 10px; @@ -99,4 +128,8 @@ background: transparent; border: 0; outline: 0; + + @media (min-width: $screen-sm-min) { + right: 160px; + } } diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 7a90713dd3f62ce1d2beef58d86ce863c69e22f2..5a9f199fb34ba4940db0056341b83f05409a5fb9 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -274,7 +274,7 @@ $colors: ( } .discard-changes-alert { - background-color: $background-color; + background-color: $gray-light; text-align: right; padding: $gl-padding-top $gl-padding; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6234779ac1913b6295ff7cad101c2fa56db690b4..45ff9f7ff5f34343bff8f0625270953273a680a0 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,8 +3,8 @@ * */ .mr-state-widget { - background: $background-color; - color: $gl-gray; + background: $gray-light; + color: $gl-text-color; border: 1px solid $border-color; border-radius: 2px; @@ -21,6 +21,14 @@ display: inline-block; float: left; + .btn-success.dropdown-toggle .fa { + color: inherit; + } + + .btn-success.dropdown-toggle:disabled { + background-color: $gl-success; + } + .accept_merge_request { &.ci-pending, &.ci-running { @@ -50,7 +58,7 @@ padding-right: 0; a { - color: $gl-gray; + color: $gl-text-color; } } @@ -62,7 +70,7 @@ .ci_widget { border-bottom: 1px solid $well-inner-border; - color: $gl-gray; + color: $gl-text-color; svg { margin-right: 4px; @@ -86,7 +94,7 @@ } .normal { - color: $gl-text-color-dark; + color: $gl-text-color; } .js-deployment-link { @@ -96,9 +104,9 @@ .mr-widget-body { h4 { font-weight: 600; - font-size: 17px; + font-size: 16px; margin: 5px 0; - color: $gl-gray-dark; + color: $gl-text-color; &.has-conflicts .fa-exclamation-triangle { color: $gl-warning; @@ -182,7 +190,7 @@ } .label-branch { - color: $gl-gray-dark; + color: $gl-text-color; font-family: $monospace_font; font-weight: bold; overflow: hidden; @@ -302,10 +310,6 @@ left: 0; top: 2px; } - - .commit-row-info { - line-height: 20px; - } } .btn-clipboard { @@ -359,7 +363,7 @@ th { background-color: $white-light; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } } } @@ -375,7 +379,7 @@ } .mr-version-controls { - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; color: $gl-text-color; @@ -416,11 +420,19 @@ .merge-request-tabs-holder { background-color: $white-light; + .container-limited { + max-width: $limited-layout-width; + } + &.affix { top: 100px; left: 0; z-index: 10; transition: right .15s; + + @media (max-width: $screen-xs-max) { + right: 0; + } } &:not(.affix) .container-fluid { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index dfc6079bd151df951b5bc9034bcf90bcedae5992..686b64cdd24acfe8b49fe753171bd05728a105d5 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -25,12 +25,6 @@ } .issuable-row { - .color-label { - border-radius: 2px; - padding: 3px !important; - margin-right: 7px; - } - span a { color: $gl-text-color; word-wrap: break-word; @@ -108,13 +102,17 @@ margin-top: 7px; .issuable-number { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; margin-right: 5px; } .avatar { float: none; } + + > a:not(:last-of-type) { + margin-right: 5px; + } } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index c35d71f9e7ba887b60b5002f8619215f7229c61a..f984b469609731a480e3cfc5aea03d4edb557bda 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -27,6 +27,7 @@ .new-note, .note-edit-form { .note-form-actions { + position: relative; margin-top: $gl-padding; } @@ -44,7 +45,7 @@ .note-textarea { display: block; padding: 10px 0; - color: $gl-gray; + color: $gl-text-color; font-family: $regular_font; border: 0; @@ -62,7 +63,7 @@ .common-note-form { .md-area { padding: $gl-padding-top $gl-padding; - border: 1px solid $note-form-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; @@ -109,7 +110,7 @@ margin: auto; margin-top: 0; text-align: center; - font-size: 13px; + font-size: 12px; @media (max-width: $screen-sm-max) { // On smaller devices the warning becomes the fourth item in the list, @@ -204,7 +205,7 @@ .comment-toolbar { padding-top: $gl-padding-top; - color: $note-toolbar-color; + color: $gl-text-color-secondary; border-top: 1px solid $border-color; } @@ -265,3 +266,18 @@ } } } + +.note-edit-warning.settings-message { + display: none; + padding: 5px 10px; + position: absolute; + left: 127px; + top: 2px; + + @media (max-width: $screen-xs-max) { + position: relative; + top: 0; + left: 0; + margin-bottom: 10px; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 10eb3d4203ee9e2d30f5dbc2f22038c92b73ebd5..e2a0253da388c36921a26758ed028c3399a5594d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -43,7 +43,7 @@ ul.notes { } .system-note-message { - display: inline-block; + display: inline; &::first-letter { text-transform: lowercase; @@ -55,7 +55,7 @@ ul.notes { } p { - display: inline-block; + display: inline; margin: 0; &::first-letter { @@ -166,7 +166,7 @@ ul.notes { .note { display: block; position: relative; - border-bottom: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; &.note-discussion { &.timeline-entry { @@ -291,14 +291,14 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: $background-color; + background: $gray-light; color: $text-color; } @@ -309,7 +309,7 @@ ul.notes { } &.notes_content { - background-color: $background-color; + background-color: $gray-light; border-width: 1px 0; padding: 0; vertical-align: top; @@ -345,7 +345,7 @@ ul.notes { } .author_link { - color: $gl-gray; + color: $gl-text-color; } } @@ -353,6 +353,14 @@ ul.notes { font-size: 14px; } +.note-headline-light { + display: inline; + + @media (max-width: $screen-xs-min) { + display: block; + } +} + .note-headline-light, .discussion-headline-light { color: $notes-light-color; @@ -372,7 +380,7 @@ ul.notes { .note-actions { float: right; margin-left: 10px; - color: $notes-action-color; + color: $gray-darkest; } .note-actions { @@ -383,10 +391,6 @@ ul.notes { .note-action-button { margin-left: 10px; } - - @media (min-width: $screen-sm-min) { - position: relative; - } } .discussion-actions { @@ -411,7 +415,7 @@ ul.notes { } .fa { - color: $notes-action-color; + color: $gray-darkest; position: relative; font-size: 17px; } @@ -448,15 +452,10 @@ ul.notes { color: $notes-role-color; font-size: 12px; line-height: 20px; - border: 1px solid $notes-role-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; } -.diff-file .note .note-actions { - right: 0; - top: 0; -} - /** * Line note button on the side of diffs @@ -527,27 +526,24 @@ ul.notes { } .line-resolve-all { + vertical-align: middle; display: inline-block; - padding: 5px 10px; - background-color: $background-color; + padding: 6px 10px; + background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; &.has-next-btn { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-right: 0; } .line-resolve-btn { - vertical-align: middle; margin-right: 5px; } } -.line-resolve-text { - vertical-align: middle; -} - .line-resolve-btn { display: inline-block; position: relative; @@ -566,18 +562,14 @@ ul.notes { &.is-active { color: $gl-text-green; - svg path { + svg { fill: $gl-text-green; } } svg { position: relative; - color: $notes-action-color; - - path { - fill: $notes-action-color; - } + fill: $gray-darkest; } } @@ -590,3 +582,17 @@ ul.notes { } } } + +// Merge request notes in diffs +.diff-file { + // Diff is side by side + .notes_content.parallel .note-header .note-headline-light { + display: block; + position: relative; + } + // Diff is inline + .notes_content .note-header .note-headline-light { + display: inline-block; + position: relative; + } +} diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index 7d61390a439915cebad2313c3a223864d77632ed..bdf07a99dafb3d03999df68a2d5c72651b17ec59 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -10,19 +10,7 @@ position: relative; top: 1px; - > .fa { + .fa { font-size: 18px; } } - -.ns-part { - color: $gl-text-green; -} - -.ns-watch { - color: $gl-success; -} - -.ns-mute { - color: $gl-danger; -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 6822f916cc5b7b882ae7becf928d918895ef532a..8861315d776dcb1f50ae73afd2d42088c5f9fc02 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,4 +1,9 @@ .pipelines { + .realtime-loading { + font-size: 40px; + text-align: center; + } + .stage { max-width: 90px; width: 90px; @@ -22,17 +27,26 @@ .table.ci-table { min-width: 1200px; + table-layout: fixed; + + .label { + margin-bottom: 3px; + } .pipeline-id { color: $black; } - .branch-commit { - width: 30%; + .pipeline-date, + .pipeline-status { + width: 10%; + } - .branch-name { - max-width: 195px; - } + .pipeline-info, + .pipeline-commit, + .pipeline-actions, + .pipeline-stages { + width: 20%; } } } @@ -47,6 +61,10 @@ } } +.content-list.pipelines .table-holder { + min-height: 300px; +} + .pipeline-holder { width: 100%; overflow: auto; @@ -75,6 +93,10 @@ td { padding: 10px 8px; } + + .commit-link { + padding: 9px 8px 10px; + } } tbody { @@ -106,7 +128,7 @@ .branch-name { font-weight: bold; - max-width: 150px; + max-width: 120px; overflow: hidden; display: inline-block; white-space: nowrap; @@ -118,7 +140,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-text-color-secondary; } .fa { @@ -132,7 +154,7 @@ .commit-title { margin-top: 4px; - max-width: 300px; + max-width: 225px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -164,12 +186,14 @@ .stage-cell { font-size: 0; - svg { - height: 18px; - width: 18px; - position: relative; + > .stage-container > div > button > span > svg, + > .stage-container > button > svg { + height: 22px; + width: 22px; + position: absolute; + top: -1px; + left: -1px; z-index: 2; - vertical-align: middle; overflow: visible; } @@ -187,21 +211,17 @@ content: ''; width: 8px; position: absolute; - right: -7px; - bottom: 9px; + right: -8px; + top: 10px; border-bottom: 2px solid $border-color; } } - - a { - display: block; - } } } .duration, .finished-at { - color: $table-text-gray; + color: $gl-text-color-secondary; margin: 4px 0; .fa { @@ -222,7 +242,7 @@ .btn { margin: 0; - color: $table-text-gray; + color: $gl-text-color-secondary; } .cancel-retry-btns { @@ -235,10 +255,10 @@ .dropdown-toggle, .dropdown-menu { - color: $table-text-gray; + color: $gl-text-color-secondary; .fa { - color: $table-text-gray; + color: $gl-text-color-secondary; font-size: 14px; } @@ -272,7 +292,7 @@ .build-link { a { - color: $gl-dark-link-color; + color: $gl-text-color; } } @@ -288,69 +308,162 @@ } // Pipeline visualization +.pipeline-actions { + border-bottom: none; +} + +.tab-pane { + &.pipelines { + .ci-table { + min-width: 900px; + } -.toggle-pipeline-btn { - background-color: $gray-dark; + .content-list.pipelines { + overflow: auto; + } - &.graph-collapsed { - background-color: $white-light; + .stage { + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } + } + + &.builds { + .ci-table { + tr { + height: 71px; + } + } } } +// Pipeline graph .pipeline-graph { width: 100%; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; - overflow: auto; white-space: nowrap; transition: max-height 0.3s, padding 0.3s; - &.graph-collapsed { - max-height: 0; - padding: 0 16px; + .stage-column-list, + .builds-container > ul { + padding: 0; } -} -.pipeline-visualization { - position: relative; + a { + text-decoration: none; + color: $gl-text-color-secondary; + } - ul { - padding: 0; + svg { + vertical-align: middle; + margin-right: 3px; } -} -.stage-column { - display: inline-block; - vertical-align: top; + .stage-column { + display: inline-block; + vertical-align: top; - &:not(:last-child) { - margin-right: 44px; - } + &:not(:last-child) { + margin-right: 44px; + } - &.left-margin { - &:not(:first-child) { - margin-left: 44px; + &.left-margin { + &:not(:first-child) { + margin-left: 44px; - .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -48px; - border-top: 2px solid $border-color; - width: 48px; - height: 1px; + .left-connector { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -48px; + border-top: 2px solid $border-color; + width: 48px; + height: 1px; + } } } } - } - &.no-margin { - margin: 0; - } + &.no-margin { + margin: 0; + } - li { - list-style: none; + li { + list-style: none; + } + + &:last-child { + .build { + // Remove right connecting horizontal line from first build in last stage + &:first-child { + &::after { + border: none; + } + } + // Remove right curved connectors from all builds in last stage + &:not(:first-child) { + &::after { + border: none; + } + } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } + } + } + + &:first-child { + .build { + // Remove left curved connectors from all builds in first stage + &:not(:first-child) { + &::before { + border: none; + } + } + // Remove opposite curve + .curve { + &::after { + display: none; + } + } + } + } + + // Curve first child connecting lines in opposite direction + .curve { + display: none; + + &::before, + &::after { + content: ''; + width: 21px; + height: 25px; + position: absolute; + top: -31px; + border-top: 2px solid $border-color; + } + + &::after { + left: -44px; + border-right: 2px solid $border-color; + border-radius: 0 20px; + } + + &::before { + right: -44px; + border-left: 2px solid $border-color; + border-radius: 20px 0 0; + } + } } .stage-name { @@ -363,167 +476,114 @@ } .build { - border: 1px solid $border-color; - background-color: $white-light; position: relative; - padding: 7px 10px 8px; - border-radius: 30px; width: 186px; margin-bottom: 10px; + white-space: normal; + color: $gl-text-color-secondary; + + // Action Icons in big pipeline-graph nodes + > .ci-action-icon-container .ci-action-icon-wrapper { + i { + color: $border-color; + border-radius: 100%; + border: 1px solid $border-color; + padding: 5px 6px; + font-size: 13px; + background: $white-light; + height: 30px; + width: 30px; - &:hover { - background-color: $gray-lighter; - } - - &.playable { - - svg { - height: 13px; - width: 20px; - position: relative; - top: 1px; - - path { - fill: $layout-link-gray; - } - } - } - - .build-content { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - width: 164px; - - .ci-status-icon { - svg { - height: 20px; - width: 20px; + &::before { + position: relative; + top: 3px; + left: 3px; } - } - - .tooltip { - white-space: nowrap; - .tooltip-inner { - overflow: hidden; - text-overflow: ellipsis; + &:hover { + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; } } - .ci-status-text { - width: 135px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - display: inline-block; - position: relative; - top: -1px; - } - - a { - color: $gl-text-color-light; - text-decoration: none; + .ci-play-icon { + padding: 5px 5px 5px 7px; } + } - .dropdown-menu-toggle { - background-color: transparent; - border: none; - width: auto; - padding: 0; - color: $gl-text-color-light; - flex-grow: 1; - - .ci-status-text { - max-width: 112px; - width: auto; - } - } + > .ci-action-icon-container { + position: absolute; + right: 5px; + top: 5px; + } - .grouped-pipeline-dropdown { - padding: 0; - width: 186px; - left: auto; - right: -197px; - top: -9px; + .ci-status-icon svg { + height: 20px; + width: 20px; + } - ul { - max-height: 245px; - overflow: auto; + .dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-secondary; - li:first-child { - padding-top: 8px; - } + &:focus { + outline: none; + } - li:last-child { - padding-bottom: 8px; - } - } + &:hover { + color: $gl-text-color; - a { + .dropdown-counter-badge { color: $gl-text-color; - padding: 7px 8px 8px; - - &:hover { - background-color: $blue-light-transparent; - border-radius: 3px; - - .ci-status-text { - text-decoration: none; - } - } - } - - svg { - width: 14px; - height: 14px; } + } + } - .ci-status-text { - width: 112px; - } + > .build-content { + display: inline-block; + padding: 8px 10px 9px; + width: 100%; + border: 1px solid $border-color; + border-radius: 30px; + background-color: $white-light; - .arrow { - &::before, - &::after { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 18px; - } + &:hover { + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-border; + color: $gl-text-color; + } + } - &::before { - left: -5px; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $border-color; - } - &::after { - left: -4px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - } - } + .arrow { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 18px; } - .badge { - background-color: $gray-darker; - color: $gl-text-color-light; - font-weight: normal; - margin-left: $btn-xs-side-margin; + &::before { + left: -5px; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: $border-color; } - } - svg { - vertical-align: middle; - margin-right: 5px; + &::after { + left: -4px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: $white-light; + } } // Connect first build in each stage with right horizontal line @@ -579,110 +639,302 @@ } } } +} - &:last-child { - .build { - // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: none; - } - } - // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: none; - } - } - // Remove opposite curve - .curve { - &::before { - display: none; - } - } +// Triggers the dropdown in the big pipeline graph +.dropdown-counter-badge { + color: $border-color; + font-weight: 100; + font-size: 15px; + position: absolute; + right: 5px; + top: 8px; +} + +.ci-status-text { + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + display: inline-block; + position: relative; + font-weight: 200; +} + +// Dropdown button in mini pipeline graph +.mini-pipeline-graph-dropdown-toggle { + border-radius: 100px; + background-color: $white-light; + border-width: 1px; + border-style: solid; + width: 22px; + height: 22px; + margin: 0; + padding: 0; + transition: all 0.2s linear; + position: relative; + + > .fa.fa-caret-down { + position: absolute; + left: 20px; + top: 5px; + display: inline-block; + visibility: hidden; + opacity: 0; + color: inherit; + font-size: 12px; + transition: visibility 0.1s, opacity 0.1s linear; + } + + &:active, + &:focus, + &:hover { + outline: none; + width: 35px; + + .fa.fa-caret-down { + visibility: visible; + opacity: 1; } } - &:first-child { - .build { - // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: none; - } - } - // Remove opposite curve - .curve { - &::after { - display: none; - } - } + // Dropdown button animation in mini pipeline graph + &.ci-status-icon-success { + border-color: $gl-success; + color: $gl-success; + + &:hover, + &:focus, + &:active { + background-color: rgba($gl-success, 0.1); + border-color: $gl-success; } } - // Curve first child connecting lines in opposite direction - .curve { - display: none; + &.ci-status-icon-failed { + border-color: $gl-danger; + color: $gl-danger; - &::before, - &::after { - content: ''; - width: 21px; - height: 25px; - position: absolute; - top: -32px; - border-top: 2px solid $border-color; + &:hover, + &:focus, + &:active { + background-color: rgba($gl-danger, 0.1); + border-color: $gl-danger; } + } - &::after { - left: -44px; - border-right: 2px solid $border-color; - border-radius: 0 20px; + &.ci-status-icon-pending, + &.ci-status-icon-success_with_warnings { + border-color: $gl-warning; + color: $gl-warning; + + &:hover, + &:focus, + &:active { + background-color: rgba($gl-warning, 0.1); + border-color: $gl-warning; } + } - &::before { - right: -44px; - border-left: 2px solid $border-color; - border-radius: 20px 0 0; + &.ci-status-icon-running { + border-color: $blue-normal; + color: $blue-normal; + + &:hover, + &:focus, + &:active { + background-color: rgba($blue-normal, 0.1); + border-color: $blue-normal; } } -} -.pipeline-actions { - border-bottom: none; + &.ci-status-icon-canceled, + &.ci-status-icon-disabled, + &.ci-status-icon-not-found, + &.ci-status-icon-manual { + border-color: $gl-text-color; + color: $gl-text-color; + + &:hover, + &:focus, + &:active { + background-color: rgba($gl-text-color, 0.1); + border-color: $gl-text-color; + } + } + + &.ci-status-icon-created, + &.ci-status-icon-skipped { + border-color: $gray-darkest; + color: $gray-darkest; + + &:hover, + &:focus, + &:active { + background-color: rgba($gray-darkest, 0.1); + border-color: $gray-darkest; + } + } } -.toggle-pipeline-btn { +// dropdown content for big and mini pipeline +.big-pipeline-graph-dropdown-menu, +.mini-pipeline-graph-dropdown-menu { + width: 195px; + max-width: 195px; - .fa { - color: $dropdown-header-color; + li { + padding: 2px 3px; + } + + .scrollable-menu { + max-height: 245px; + overflow: auto; + } + + // Loading icon + .builds-dropdown-loading { + margin: 0 auto; + width: 20px; + } + + // Action icon on the right + a.ci-action-icon-wrapper { + color: $action-icon-color; + border: 1px solid $action-icon-color; + border-radius: 20px; + width: 22px; + height: 22px; + padding: 2px 0 0 5px; + cursor: pointer; + float: right; + margin: -26px 9px 0 0; + font-size: 12px; + background-color: $white-light; + + &:hover, + &:focus { + text-decoration: none; + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid transparent; + } + } + + // link to the build + .mini-pipeline-graph-dropdown-item { + padding: 3px 7px 4px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + white-space: nowrap; + margin: 0 5px; + border-radius: 3px; + + // build name + .ci-build-text { + font-weight: 200; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 90px; + color: $gl-text-color-secondary; + margin-left: 2px; + display: inline-block; + top: 1px; + vertical-align: text-bottom; + position: relative; + } + + // status icon on the left + .ci-status-icon { + top: 3px; + position: relative; + + > svg { + overflow: visible; + width: 18px; + height: 18px; + } + } + + &:hover, + &:focus { + outline: none; + text-decoration: none; + color: $gl-text-color; + background-color: $stage-hover-bg; + } } } -.tab-pane { +// Dropdown in the big pipeline graph +.big-pipeline-graph-dropdown-menu { + width: 195px; + min-width: 195px; + left: auto; + right: -195px; + top: -4px; + box-shadow: 0 1px 5px $black-transparent; - &.pipelines { + .mini-pipeline-graph-dropdown-item { + .ci-status-icon { + top: -1px; + } + } +} - .ci-table { - min-width: 900px; +/** + * Top arrow in the dropdown in the mini pipeline graph + */ +.mini-pipeline-graph-dropdown-menu { + .arrow-up { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: -6px; + left: 2px; + border-width: 0 5px 6px; } - .stage { - max-width: 100px; - width: 100px; + &::before { + border-width: 0 5px 5px; + border-bottom-color: $border-color; } - .pipeline-actions { - min-width: initial; + &::after { + margin-top: 1px; + border-bottom-color: $white-light; } } +} + +/** + * Terminal + */ +.terminal-icon { + margin-left: 3px; +} + +.terminal-container { + .content-block { + border-bottom: none; + } - &.builds { + #terminal { + margin-top: 10px; + min-height: 450px; + box-sizing: border-box; - .ci-table { - tr { - height: 71px; - } + > div { + min-height: 450px; } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f8677f93fe045b8bb5124b91f2e6f163705421bb..722b3006f7c870b12e62e32228aa766dd02b4c42 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -60,8 +60,8 @@ .account-well { padding: 10px; - background-color: $help-well-bg; - border: 1px solid $help-well-border; + background-color: $gray-light; + border: 1px solid $border-color; border-radius: $border-radius-base; ul { @@ -136,7 +136,7 @@ .provider-btn-group { display: inline-block; margin-right: 10px; - border: 1px solid $provider-btn-group-border; + border: 1px solid $border-color; border-radius: 3px; &:last-child { @@ -147,7 +147,7 @@ .provider-btn-image { display: inline-block; padding: 5px 10px; - border-right: 1px solid $provider-btn-group-border; + border-right: 1px solid $border-color; > img { width: 20px; @@ -198,7 +198,7 @@ } .personal-access-tokens-never-expires-label { - color: $personal-access-tokens-disabled-label-color; + color: $note-disabled-comment-color; } .datepicker.personal-access-tokens-expires-at .ui-state-disabled span { @@ -216,8 +216,8 @@ } } -.user-profile { +.user-profile { .cover-controls a { margin-left: 5px; } @@ -231,8 +231,11 @@ } } - @media (max-width: $screen-xs-max) { + .user-profile-nav { + font-size: 0; + } + @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; } @@ -253,6 +256,12 @@ } } } + + .user-profile-nav { + a { + margin-right: 0; + } + } } } @@ -262,3 +271,13 @@ table.u2f-registrations { border-right: solid 1px transparent; } } + +.oauth-application-show { + .scope-name { + font-weight: 600; + } + + .scopes-list { + padding-left: 18px; + } +} diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index f8da0983b7709b6cb59a3dea01b04c133649cce3..100ace41f2aed1f56794388cf989498ad380f08b 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -22,8 +22,8 @@ background: $theme-graphite; } - &.ui_gray { - background: $theme-gray; + &.ui_black { + background: $theme-black; } &.ui_green { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6e0f6b1cd8130277e2abb4303387d6f93e1f35dd..9455ba3b98aaf605f83b837500a7bac8c6b44336 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -6,12 +6,6 @@ } } -.no-ssh-key-message, -.project-limit-message { - background-color: $project-limit-message-bg; - margin-bottom: 0; -} - .new_project, .edit-project { @@ -32,7 +26,7 @@ margin-bottom: 5px; } - &> .form-group { + & > .form-group { padding-left: 0; } } @@ -79,7 +73,7 @@ border: 1px solid $border-color; } - &+ .select2 a { + & + .select2 a { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -99,7 +93,6 @@ .group-avatar { float: none; margin: 0 auto; - border: none; &.identicon { border-radius: 50%; @@ -151,8 +144,6 @@ .project-repo-buttons, .group-buttons { - margin-top: 15px; - .btn { @include btn-gray; padding: 3px 10px; @@ -181,11 +172,9 @@ } } - .download-button, - .dropdown-toggle, - .notification-dropdown, - .project-dropdown { - margin-left: 10px; + .project-action-button { + margin: 15px 5px 0; + vertical-align: top; } .notification-dropdown .dropdown-menu { @@ -201,13 +190,15 @@ .count-buttons { display: inline-block; vertical-align: top; + margin-top: 15px; } .project-clone-holder { display: inline-block; + margin: 15px 5px 0 0; input { - height: 29px; + height: 28px; } } @@ -261,7 +252,7 @@ line-height: 13px; padding: $gl-vert-padding $gl-padding; letter-spacing: .4px; - padding: 7px 14px; + padding: 6px 14px; text-align: center; vertical-align: middle; touch-action: manipulation; @@ -301,7 +292,7 @@ .option-title { font-weight: normal; display: inline-block; - color: $gl-gray-dark; + color: $gl-text-color; } .option-descr { @@ -340,7 +331,7 @@ a.deploy-project-label { padding: 5px; margin-right: 5px; - color: $gl-gray; + color: $gl-text-color; background-color: $row-hover; &:hover { @@ -381,7 +372,7 @@ a.deploy-project-label { } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .dropdown-menu { @@ -420,13 +411,13 @@ a.deploy-project-label { width: 100px; height: 100px; background-color: $gray-light; - border: 1px solid $gray-dark; + border: 1px solid $white-normal; margin: 0 auto; border-radius: 50%; i { font-size: 100px; - color: $gray-dark; + color: $white-normal; } } @@ -435,7 +426,7 @@ a.deploy-project-label { width: 100%; height: 100%; padding-top: $gl-padding; - color: $gl-gray; + color: $gl-text-color; .caption { min-height: 30px; @@ -498,6 +489,7 @@ a.deploy-project-label { .project-stats { font-size: 0; + text-align: center; border-bottom: 1px solid $border-color; .nav { @@ -536,7 +528,7 @@ a.deploy-project-label { } li.missing { - border: 1px dashed $border-gray-light; + border: 1px dashed $border-gray-normal; border-radius: $border-radius-default; a { @@ -560,7 +552,7 @@ pre.light-well { margin: 0 7px 7px; h5 { - color: $gl-text-color-dark; + color: $gl-text-color; } .light-well { @@ -591,15 +583,25 @@ pre.light-well { @include basic-list; .project-row { - border-color: $table-border-color; + border-color: $white-normal; .project-full-name { @include str-truncated; + + @media (max-width: $screen-xs-max) { + max-width: 50%; + } } .controls { line-height: $list-text-height; + .badge { + @media (max-width: $screen-xs-max) { + display: none; + } + } + a:hover { text-decoration: none; } @@ -613,6 +615,12 @@ pre.light-well { top: 2px; } } + + .description p { + @media (max-width: $screen-xs-max) { + max-width: 50%; + } + } } .bottom { @@ -626,7 +634,6 @@ pre.light-well { margin: 0; } - .activity-filter-block { .controls { padding-bottom: 7px; @@ -635,6 +642,12 @@ pre.light-well { } } +.commits-search-form { + .input-short { + min-width: 200px; + } +} + .project-last-commit { @media (min-width: $screen-sm-min) { margin-top: $gl-padding; @@ -643,7 +656,7 @@ pre.light-well { &.container-fluid { padding-top: 12px; padding-bottom: 12px; - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; border-right-width: 0; border-left-width: 0; @@ -665,7 +678,7 @@ pre.light-well { } .commit-row-message { - color: $gl-gray; + color: $gl-text-color; } .commit_short_id { @@ -753,7 +766,7 @@ pre.light-well { .protected-branches-list { a { - color: $gl-gray; + color: $gl-text-color; &:hover { color: $gl-link-color; @@ -813,7 +826,31 @@ pre.light-well { .compare-form-group { .dropdown-menu { - width: 300px; + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 300px; + } + } + + + .compare-ellipsis { + width: 100%; + vertical-align: middle; + text-align: center; + margin-top: -20px; + + @media (min-width: $screen-sm-min) { + margin-top: 0; + width: auto; + } + } + + .inline-input-group { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 250px; + } } } @@ -888,3 +925,23 @@ pre.light-well { width: 30%; } } + +.services-installation-info .row { + margin-bottom: 10px; +} + +.service-installation { + padding: 32px; + margin: 32px; + border-radius: 3px; + background-color: $white-light; + + h3 { + margin-top: 0; + } + + hr { + margin: 32px 0; + border-color: $border-color; + } +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 63d0a34e610fa01687f484ab4ea041f0e6b53b0e..12bff32bbf38643e3c85d78662ee98cb519adcb0 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -14,6 +14,20 @@ } } +.search form:hover, +.file-finder-input:hover, +.issuable-search-form:hover, +.search-text-input:hover, +textarea:hover, +.form-control:hover { + border-color: lighten($dropdown-input-focus-border, 20%); + box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); +} + +input[type="checkbox"]:hover { + box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%); +} + .search { margin-right: 10px; margin-left: 10px; @@ -51,9 +65,9 @@ border-radius: $border-radius-default; font-size: 14px; font-style: normal; - color: $location-badge-color; + color: $note-disabled-comment-color; display: inline-block; - background-color: $location-badge-bg; + background-color: $gray-normal; vertical-align: top; cursor: default; } @@ -140,7 +154,7 @@ .search-input-wrap { i { - color: $location-icon-active-color; + color: $layout-link-gray; } } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 51c926608f9af300a93e4a804e85784b5201e6aa..a28a87ed4f8b16e0f97cf77c0f783800e9c4f367 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -1,5 +1,5 @@ .settings-list-icon { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; font-size: $settings-icon-size; line-height: 42px; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 5084b466722e7f9f23884561245327b829c58e6d..f19275770be9dfcc65d5e1c34bdb3448b064be29 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,6 +1,6 @@ .container-fluid { .ci-status { - padding: 2px 7px; + padding: 2px 7px 4px; margin-right: 10px; border: 1px solid $gray-darker; white-space: nowrap; @@ -15,8 +15,7 @@ height: 13px; width: 13px; position: relative; - top: 1px; - margin-right: 3px; + top: 2px; overflow: visible; } @@ -25,7 +24,7 @@ border-color: $gl-danger; &:not(span):hover { - background-color: rgba( $gl-danger, .07); + background-color: rgba($gl-danger, .07); } svg { @@ -39,7 +38,7 @@ border-color: $gl-success; &:not(span):hover { - background-color: rgba( $gl-success, .07); + background-color: rgba($gl-success, .07); } svg { @@ -52,7 +51,7 @@ border-color: $gl-info; &:not(span):hover { - background-color: rgba( $gl-info, .07); + background-color: rgba($gl-info, .07); } svg { @@ -62,15 +61,15 @@ &.ci-canceled, &.ci-disabled { - color: $gl-gray; - border-color: $gl-gray; + color: $gl-text-color; + border-color: $gl-text-color; &:not(span):hover { - background-color: rgba( $gl-gray, .07); + background-color: rgba($gl-text-color, .07); } svg { - fill: $gl-gray; + fill: $gl-text-color; } } @@ -79,7 +78,7 @@ border-color: $gl-warning; &:not(span):hover { - background-color: rgba( $gl-warning, .07); + background-color: rgba($gl-warning, .07); } svg { @@ -92,7 +91,7 @@ border-color: $blue-normal; &:not(span):hover { - background-color: rgba( $blue-normal, .07); + background-color: rgba($blue-normal, .07); } svg { @@ -102,15 +101,28 @@ &.ci-created, &.ci-skipped { - color: $table-text-gray; - border-color: $table-text-gray; + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; &:not(span):hover { - background-color: rgba( $table-text-gray, .07); + background-color: rgba($gl-text-color-secondary, .07); } svg { - fill: $table-text-gray; + fill: $gl-text-color-secondary; + } + } + + &.ci-manual { + color: $gl-text-color; + border-color: $gl-text-color; + + &:not(span):hover { + background-color: rgba($gl-text-color, .07); + } + + svg { + fill: $gl-text-color; } } } @@ -123,3 +135,9 @@ left: 5px; } } + +.ci-status-link { + svg { + overflow: visible; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 508b30f394787640f040d51293aa9cfe61636882..01675acc62e9d5fdc8c544928c3f191813c6bee6 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -90,7 +90,7 @@ } p { - color: $gl-text-color-dark; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 20ad63be0458014cd4ba662f373a177330a4bd15..4cce1c363ebfaa9e522052401925fcc1c62b9046 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -14,14 +14,15 @@ .add-to-tree { vertical-align: top; + padding: 6px 10px; } .tree-table { margin-bottom: 0; tr { - border-bottom: 1px solid $table-border-gray; - border-top: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; + border-top: 1px solid $white-normal; td, th { @@ -39,7 +40,7 @@ .commit-history-link-spacer { margin: 0 10px; - color: $table-border-color; + color: $white-normal; } &:hover { @@ -53,7 +54,7 @@ &.selected { td { - background: $gray-dark; + background: $white-normal; border-top: 1px solid $border-gray-dark; border-bottom: 1px solid $border-gray-dark; } @@ -77,7 +78,7 @@ i, a { - color: $gl-dark-link-color; + color: $gl-text-color; } img { @@ -103,21 +104,21 @@ padding-right: 8px; .commit-author-name { - color: $gl-gray; + color: $gl-text-color; } } .tree-time-ago { min-width: 135px; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .tree-commit { max-width: 320px; - color: $gl-gray-light; + color: $gl-text-color-secondary; .tree-commit-link { - color: $gl-gray-light; + color: $gl-text-color-secondary; &:hover { text-decoration: underline; @@ -133,21 +134,18 @@ .blob-commit-info { list-style: none; - padding: $gl-padding; - background: $background-color; + background: $gray-light; + padding: 6px 0; border: 1px solid $border-color; border-bottom: none; margin: 0; - .commit { - padding-top: 0; - padding-bottom: 0; + .table-list-cell { + border-bottom: none; + } - .commit-row-title { - .commit-row-message { - font-weight: normal; - } - } + .commit-actions { + width: 200px; } } @@ -172,7 +170,7 @@ position: relative; z-index: 2; - .download-button { + .project-action-button { margin-left: $btn-side-margin; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index b9f8153315025829a6435861394e0af078fe62ae..d5783e14b21502ce324fdf91366603fd10a18363 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -15,7 +15,7 @@ } .wiki-last-edit-by { - color: $gl-gray-light; + color: $gl-text-color-secondary; strong { color: $gl-text-color; @@ -24,7 +24,7 @@ .light { font-weight: normal; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .git-access-header { diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 9f9d630978a6be5567a30550ec8b641b04a21879..b085c56390de8299d16aef8318a39fd3529475f7 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -18,7 +18,7 @@ $l-blue: #81a2be; $l-magenta: #b294bb; $l-cyan: #8abeb7; - $l-white: $ci-text-color; + $l-white: $gray-darkest; /* * xterm colors diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c2bb846482479b9f25e5f7b12c2613c1e68e117a..1b4987dd7382a4de6fe3016ab5025f5b9c566424 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -67,69 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.require(:application_setting).permit( - :default_projects_limit, - :default_branch_protection, - :signup_enabled, - :signin_enabled, - :require_two_factor_authentication, - :two_factor_grace_period, - :gravatar_enabled, - :sign_in_text, - :after_sign_up_text, - :help_page_text, - :home_page_url, + application_setting_params_ce + ) + end + + def application_setting_params_ce + [ + :admin_notification_email, :after_sign_out_path, - :max_attachment_size, - :session_expire_delay, + :after_sign_up_text, + :akismet_api_key, + :akismet_enabled, + :container_registry_token_expire_delay, + :default_branch_protection, + :default_group_visibility, :default_project_visibility, + :default_projects_limit, :default_snippet_visibility, - :default_group_visibility, - :domain_whitelist_raw, :domain_blacklist_enabled, - :domain_blacklist_raw, :domain_blacklist_file, - :version_check_enabled, - :admin_notification_email, - :user_oauth_applications, - :user_default_external, - :shared_runners_enabled, - :shared_runners_text, + :domain_blacklist_raw, + :domain_whitelist_raw, + :email_author_in_body, + :enabled_git_access_protocol, + :gravatar_enabled, + :help_page_text, + :home_page_url, + :housekeeping_bitmaps_enabled, + :housekeeping_enabled, + :housekeeping_full_repack_period, + :housekeeping_gc_period, + :housekeeping_incremental_repack_period, + :html_emails_enabled, + :koding_enabled, + :koding_url, + :plantuml_enabled, + :plantuml_url, :max_artifacts_size, + :max_attachment_size, :metrics_enabled, :metrics_host, - :metrics_port, - :metrics_pool_size, - :metrics_timeout, :metrics_method_call_threshold, + :metrics_packet_size, + :metrics_pool_size, + :metrics_port, :metrics_sample_interval, + :metrics_timeout, :recaptcha_enabled, - :recaptcha_site_key, :recaptcha_private_key, - :sentry_enabled, - :sentry_dsn, - :akismet_enabled, - :akismet_api_key, - :koding_enabled, - :koding_url, - :email_author_in_body, - :html_emails_enabled, + :recaptcha_site_key, :repository_checks_enabled, - :metrics_packet_size, + :require_two_factor_authentication, + :session_expire_delay, + :sign_in_text, + :signin_enabled, + :signup_enabled, + :sentry_dsn, + :sentry_enabled, :send_user_confirmation_email, - :container_registry_token_expire_delay, - :enabled_git_access_protocol, + :shared_runners_enabled, + :shared_runners_text, :sidekiq_throttling_enabled, :sidekiq_throttling_factor, - :housekeeping_enabled, - :housekeeping_bitmaps_enabled, - :housekeeping_incremental_repack_period, - :housekeeping_full_repack_period, - :housekeeping_gc_period, + :two_factor_grace_period, + :user_default_external, + :user_oauth_applications, + :version_check_enabled, + + disabled_oauth_sign_in_sources: [], + import_sources: [], repository_storages: [], restricted_visibility_levels: [], - import_sources: [], - disabled_oauth_sign_in_sources: [], sidekiq_throttling_queues: [] - ) + ] end end diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 471d24934a017a5e802018a4eca7c759bd80e0f8..62f62e99a97cfc1d668afbfeeb8d3e12cd682d33 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -1,5 +1,8 @@ class Admin::ApplicationsController < Admin::ApplicationController + include OauthApplications + before_action :set_application, only: [:show, :edit, :update, :destroy] + before_action :load_scopes, only: [:new, :edit] def index @applications = Doorkeeper::Application.where("owner_id IS NULL") @@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController # Only allow a trusted parameter "white list" through. def application_params - params[:doorkeeper_application].permit(:name, :redirect_uri) + params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes) end end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index 285e84953426f2d72c19d32ade4cdb3559cd7b9b..4f6a7e9e2cbc2a069902ba9e53bc1f19d1762ae5 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create - @deploy_key = deploy_keys.new(deploy_key_params) + @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user)) if @deploy_key.save redirect_to admin_deploy_keys_path @@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController end def deploy_key_params - params.require(:deploy_key).permit(:key, :title) + params.require(:deploy_key).permit(:key, :title, :can_push) end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 1e3d194e9f9b5846fc63ee46bfbfe90740ddcb24..b7722a1d15dc1e58af01edbff63c8f30eded8377 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,17 +1,18 @@ class Admin::GroupsController < Admin::ApplicationController - before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] + before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] def index - @groups = Group.all + @groups = Group.with_statistics @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.page(params[:page]) end def show + @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id]) @members = @group.members.order("access_level DESC").page(params[:members_page]) @requesters = AccessRequestsFinder.new(@group).execute(current_user) - @projects = @group.projects.page(params[:projects_page]) + @projects = @group.projects.with_statistics.page(params[:projects_page]) end def new @@ -60,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -68,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController :path, :request_access_enabled, :visibility_level - ) + ] end end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 1d963bdf7d58df28917edef167ac5141a5e52458..b09ae4230969720c14698b5e21316162d880ae1f 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController before_action :group, only: [:show, :transfer] def index - @projects = Project.all + @projects = Project.with_statistics @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.with_push if params[:with_push].present? diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index bb912ed10cca4a264e8b64ca54036e14cd341ded..aa0f8d434dcae027ac051e76d436e2e6bf67d4c6 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -16,9 +16,6 @@ class Admin::UsersController < Admin::ApplicationController @joined_projects = user.projects.joined(@user) end - def groups - end - def keys @keys = user.keys end @@ -164,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController @user ||= User.find_by!(username: params[:id]) end - def user_params - params.require(:user).permit( - :email, :remember_me, :bio, :name, :username, - :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, - :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, - :projects_limit, :can_create_group, :admin, :key_id, :external - ) - end - def redirect_back_or_admin_user(options = {}) redirect_back_or_default(default: default_route, options: options) end @@ -180,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController def default_route [:admin, @user] end + + def user_params + params.require(:user).permit(user_params_ce) + end + + def user_params_ce + [ + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :extern_uid, + :external, + :force_random_password, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password_expires_at, + :projects_limit, + :provider, + :remember_me, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bcc0b17bce2cd4723c48c8c4f2ca11b79845a637..bb47e2a8bf7442f2a416ef0c6b0848db311af991 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('github') end + def gitea_import_enabled? + current_application_settings.import_sources.include?('gitea') + end + def github_import_configured? Gitlab::OAuth::Provider.enabled?(:github) end @@ -262,7 +266,7 @@ class ApplicationController < ActionController::Base end def bitbucket_import_configured? - Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + Gitlab::OAuth::Provider.enabled?(:bitbucket) end def google_code_import_enabled? diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 936d9bab57e76115e9cf9e3dbe591b6bc93bea62..6f43ce5226d8e375570a54c3ca110c4250306eb1 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -82,7 +82,7 @@ module CreatesCommit return @merge_request if defined?(@merge_request) @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. - find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch) + find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project) end def different_project? diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 2aaf8f2b45179b3a442b0f5a2fd5e7a5c3c8ccf6..52e06f4945a570c4c65fe381ca07b86963e1a904 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -1,6 +1,10 @@ module CycleAnalyticsParams extend ActiveSupport::Concern + def options(params) + @options ||= { from: start_date(params), current_user: current_user } + end + def start_date(params) params[:start_date] == '30' ? 30.days.ago : 90.days.ago end diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb deleted file mode 100644 index 5c503c5b69896c4e9d92477ed1817307bc9c94b7..0000000000000000000000000000000000000000 --- a/app/controllers/concerns/global_milestones.rb +++ /dev/null @@ -1,20 +0,0 @@ -module GlobalMilestones - extend ActiveSupport::Concern - - def milestones - epoch = DateTime.parse('1970-01-01') - @milestones = MilestonesFinder.new.execute(@projects, params) - @milestones = GlobalMilestone.build_collection(@milestones) - @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } - end - - def milestone - milestones = Milestone.of_projects(@projects).where(title: params[:title]) - - if milestones.present? - @milestone = GlobalMilestone.new(params[:title], milestones) - else - render_404 - end - end -end diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb new file mode 100644 index 0000000000000000000000000000000000000000..9849aa93fa61a29830c77d95ff368ced63edac7c --- /dev/null +++ b/app/controllers/concerns/oauth_applications.rb @@ -0,0 +1,19 @@ +module OauthApplications + extend ActiveSupport::Concern + + included do + before_action :prepare_scopes, only: [:create, :update] + end + + def prepare_scopes + scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) + + if scopes + params[:doorkeeper_application][:scopes] = scopes.join(' ') + end + end + + def load_scopes + @scopes = Doorkeeper.configuration.scopes + end +end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index c33d7eecb9f7941ba7f8d29d9436ee1abc652480..d7f5a4e46829810a1cbb200e5374d32f56375fa0 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -1,31 +1,72 @@ module ServiceParams extend ActiveSupport::Concern - ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :drone_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, - :colorize_messages, :channels, - # We're using `issues_events` and `merge_requests_events` - # in the view so we still need to explicitly state them - # here. `Service#event_names` would only give - # `issue_events` and `merge_request_events` (singular!) - # See app/helpers/services_helper.rb for how we - # make those event names plural as special case. - :issues_events, :confidential_issues_events, :merge_requests_events, - :notify_only_broken_builds, :notify_only_broken_pipelines, - :add_pusher, :send_from_committer_email, :disable_diffs, - :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id, :url, :project_key] + ALLOWED_PARAMS_CE = [ + :active, + :add_pusher, + :api_key, + :api_url, + :api_version, + :bamboo_url, + :build_key, + :build_type, + :ca_pem, + :channel, + :channels, + :color, + :colorize_messages, + :confidential_issues_events, + :default_irc_uri, + :description, + :device, + :disable_diffs, + :drone_url, + :enable_ssl_verification, + :external_wiki_url, + # We're using `issues_events` and `merge_requests_events` + # in the view so we still need to explicitly state them + # here. `Service#event_names` would only give + # `issue_events` and `merge_request_events` (singular!) + # See app/helpers/services_helper.rb for how we + # make those event names plural as special case. + :issues_events, + :issues_url, + :jira_issue_transition_id, + :merge_requests_events, + :namespace, + :new_issue_url, + :notify, + :notify_only_broken_builds, + :notify_only_broken_pipelines, + :password, + :priority, + :project_key, + :project_url, + :recipients, + :restrict_to_branch, + :room, + :send_from_committer_email, + :server, + :server_host, + :server_port, + :sound, + :subdomain, + :teamcity_url, + :title, + :token, + :type, + :url, + :user_key, + :username, + :webhook + ] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] def service_params dynamic_params = @service.event_channel_names + @service.event_names - service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) + service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params) if service_params[:service].is_a?(Hash) FILTER_BLANK_PARAMS.each do |param| diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index fa9c6c054f0fbb3da50aa224cf42aa345d30c9d7..7051652d109341a2576a73926797c4854cce791a 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,6 +1,4 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController - include GlobalMilestones - before_action :projects before_action :milestone, only: [:show] @@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController def show end + + private + + def milestones + @milestones = GlobalMilestone.build_collection(@projects, params) + end + + def milestone + @milestone = GlobalMilestone.build(@projects, params[:title]) + render_404 unless @milestone + end end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index d425d0f90146c918d428856ae977cabc5fdca5b6..e3933e3d7b1bc49f8d6597e3d5de10c2ab19fc5e 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController def index @sort = params[:sort] @todos = @todos.page(params[:page]) + if @todos.out_of_range? && @todos.total_pages != 0 + redirect_to url_for(params.merge(page: @todos.total_pages)) + end end def destroy diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 940a3ad20ba5495d26d759bf9664a0e0f1de568e..4f273a8d4f0af583e2cf922a2f39df67546d7bfd 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,20 +1,20 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembershipActions + include SortingHelper # Authorize before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] def index + @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] + @members = @group.group_members @members = @members.non_invite unless can?(current_user, :admin_group, @group) + @members = @members.search(params[:search]) if params[:search].present? + @members = @members.sort(@sort) + @members = @members.page(params[:page]).per(50) - if params[:search].present? - users = @group.users.search(params[:search]).to_a - @members = @members.where(user_id: users) - end - - @members = @members.order('access_level DESC').page(params[:page]).per(50) @requesters = AccessRequestsFinder.new(@group).execute(current_user) @group_member = @group.group_members.new diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 24ec4eec3f28ef52214ad849423f1c8c91ff2aec..0d872c86c8a7942496f72984b7648a36dd18bced 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,6 +1,4 @@ class Groups::MilestonesController < Groups::ApplicationController - include GlobalMilestones - before_action :group_projects before_action :milestone, only: [:show, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update] @@ -73,4 +71,13 @@ class Groups::MilestonesController < Groups::ApplicationController def milestone_path(title) group_milestone_path(@group, title.to_slug.to_s, title: title) end + + def milestones + @milestones = GroupMilestone.build_collection(@group, @projects, params) + end + + def milestone + @milestone = GroupMilestone.build(@group, @projects, params[:title]) + render_404 unless @milestone + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b83c3a872cf07bb9caaf583ae2fec7f83640fd94..f81237db9910ef5fcd799b64548e148516b42ece 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -42,6 +42,8 @@ class GroupsController < Groups::ApplicationController @notification_setting = current_user.notification_settings_for(group) end + @nested_groups = group.children + setup_projects respond_to do |format| @@ -75,13 +77,15 @@ class GroupsController < Groups::ApplicationController end def projects - @projects = @group.projects.page(params[:page]) + @projects = @group.projects.with_statistics.page(params[:page]) end def update if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else + @group.reset_path! + render action: "edit" end end @@ -121,7 +125,11 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -131,7 +139,7 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level - ) + ] end def load_events diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 6ea54744da83982edbd0633bb7e86632e6aa2a96..8e42cdf415f55c0453c4e37c0af4e11e079c3566 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController before_action :verify_bitbucket_import_enabled before_action :bitbucket_auth, except: :callback - rescue_from OAuth::Error, with: :bitbucket_unauthorized - rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized + rescue_from OAuth2::Error, with: :bitbucket_unauthorized + rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized def callback - request_token = session.delete(:oauth_request_token) - raise "Session expired!" if request_token.nil? + response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url) - request_token.symbolize_keys! - - access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url) - - session[:bitbucket_access_token] = access_token.token - session[:bitbucket_access_token_secret] = access_token.secret + session[:bitbucket_token] = response.token + session[:bitbucket_expires_at] = response.expires_at + session[:bitbucket_expires_in] = response.expires_in + session[:bitbucket_refresh_token] = response.refresh_token redirect_to status_import_bitbucket_url end def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects + bitbucket_client = Bitbucket::Client.new(credentials) + repos = bitbucket_client.repos + + @repos, @incompatible_repos = repos.partition { |repo| repo.valid? } - @already_added_projects = current_user.created_projects.where(import_type: "bitbucket") + @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket') already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } end def jobs - jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status]) - render json: jobs + render json: current_user.created_projects + .where(import_type: 'bitbucket') + .to_json(only: [:id, :import_status]) end def create + bitbucket_client = Bitbucket::Client.new(credentials) + @repo_id = params[:repo_id].to_s - repo = client.project(@repo_id.gsub('___', '/')) - @project_name = repo['slug'] - @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username']) + name = @repo_id.gsub('___', '/') + repo = bitbucket_client.repo(name) + @project_name = params[:new_name].presence || repo.name - unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute - render 'deploy_key' and return - end + repo_owner = repo.owner + repo_owner = current_user.username if repo_owner == bitbucket_client.user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = find_or_create_namespace(@target_namespace, current_user) - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + if current_user.can?(:create_projects, namespace) + # The token in a session can be expired, we need to get most recent one because + # Bitbucket::Connection class refreshes it. + session[:bitbucket_token] = bitbucket_client.connection.token + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute else render 'unauthorized' end @@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController private def client - @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token], - session[:bitbucket_access_token_secret]) + @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) + end + + def provider + Gitlab::OAuth::Provider.config_for('bitbucket') + end + + def options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys end def verify_bitbucket_import_enabled @@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController end def bitbucket_auth - if session[:bitbucket_access_token].blank? - go_to_bitbucket_for_permissions - end + go_to_bitbucket_for_permissions if session[:bitbucket_token].blank? end def go_to_bitbucket_for_permissions - request_token = client.request_token(callback_import_bitbucket_url) - session[:oauth_request_token] = request_token - - redirect_to client.authorize_url(request_token, callback_import_bitbucket_url) + redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url) end def bitbucket_unauthorized go_to_bitbucket_for_permissions end - def access_params + def credentials { - bitbucket_access_token: session[:bitbucket_access_token], - bitbucket_access_token_secret: session[:bitbucket_access_token_secret] + token: session[:bitbucket_token], + expires_at: session[:bitbucket_expires_at], + expires_in: session[:bitbucket_expires_in], + refresh_token: session[:bitbucket_refresh_token] } end end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..fbd851c64a70ccd8a0786ce317a5b0fa97590bba --- /dev/null +++ b/app/controllers/import/gitea_controller.rb @@ -0,0 +1,45 @@ +class Import::GiteaController < Import::GithubController + def new + if session[access_token_key].present? && session[host_key].present? + redirect_to status_import_url + end + end + + def personal_access_token + session[host_key] = params[host_key] + super + end + + def status + @gitea_host_url = session[host_key] + super + end + + private + + def host_key + :"#{provider}_host_url" + end + + # Overriden methods + def provider + :gitea + end + + # Gitea is not yet an OAuth provider + # See https://github.com/go-gitea/gitea/issues/27 + def logged_in_with_provider? + false + end + + def provider_auth + if session[access_token_key].blank? || session[host_key].blank? + redirect_to new_import_gitea_url, + alert: 'You need to specify both an Access Token and a Host URL.' + end + end + + def client_options + { host: session[host_key], api_version: 'v1' } + end +end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index ee7d498c59ce1cc0fe3b5e5e3d9908867abc81e0..53a5981e564ccee7d5770cf3fbb1d41c573d979a 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,39 +1,37 @@ class Import::GithubController < Import::BaseController - before_action :verify_github_import_enabled - before_action :github_auth, only: [:status, :jobs, :create] + before_action :verify_import_enabled + before_action :provider_auth, only: [:status, :jobs, :create] - rescue_from Octokit::Unauthorized, with: :github_unauthorized - - helper_method :logged_in_with_github? + rescue_from Octokit::Unauthorized, with: :provider_unauthorized def new - if logged_in_with_github? - go_to_github_for_permissions - elsif session[:github_access_token] - redirect_to status_import_github_url + if logged_in_with_provider? + go_to_provider_for_permissions + elsif session[access_token_key] + redirect_to status_import_url end end def callback - session[:github_access_token] = client.get_token(params[:code]) - redirect_to status_import_github_url + session[access_token_key] = client.get_token(params[:code]) + redirect_to status_import_url end def personal_access_token - session[:github_access_token] = params[:personal_access_token] - redirect_to status_import_github_url + session[access_token_key] = params[:personal_access_token] + redirect_to status_import_url end def status @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: "github") + @already_added_projects = current_user.created_projects.where(import_type: provider) already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } + @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end def jobs - jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status]) + jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status]) render json: jobs end @@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController namespace_path = params[:target_namespace].presence || current_user.namespace_path @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute + if can?(current_user, :create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute else render 'unauthorized' end @@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController private def client - @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token]) + @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options) end - def verify_github_import_enabled - render_404 unless github_import_enabled? + def verify_import_enabled + render_404 unless import_enabled? end - def github_auth - if session[:github_access_token].blank? - go_to_github_for_permissions - end + def go_to_provider_for_permissions + redirect_to client.authorize_url(callback_import_url) end - def go_to_github_for_permissions - redirect_to client.authorize_url(callback_import_github_url) + def import_enabled? + __send__("#{provider}_import_enabled?") end - def github_unauthorized - session[:github_access_token] = nil - redirect_to new_import_github_url, - alert: 'Access denied to your GitHub account.' + def new_import_url + public_send("new_import_#{provider}_url") end - def logged_in_with_github? - current_user.identities.exists?(provider: 'github') + def status_import_url + public_send("status_import_#{provider}_url") + end + + def callback_import_url + public_send("callback_import_#{provider}_url") + end + + def provider_unauthorized + session[access_token_key] = nil + redirect_to new_import_url, + alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account." + end + + def access_token_key + :"#{provider}_access_token" end def access_params - { github_access_token: session[:github_access_token] } + { github_access_token: session[access_token_key] } + end + + # The following methods are overriden in subclasses + def provider + :github + end + + def logged_in_with_provider? + current_user.identities.exists?(provider: provider) + end + + def provider_auth + if session[access_token_key].blank? + go_to_provider_for_permissions + end + end + + def client_options + {} end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c736200a1040d4f3cadf0c6e9b6576cd8f25bdf3..c2e4d62b50be10d5f7ed700c72b070dd3200f3a2 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -26,7 +26,7 @@ class JwtController < ApplicationController @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) render_unauthorized unless @authentication_result.success? && - (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) + (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) end rescue Gitlab::Auth::MissingPersonalTokenError render_missing_personal_token diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 0f54dfa4efc898ada20bfb20dd0c3d1819692c05..2ae4785b12cc1b43c1336240d0b5948bb75f4e96 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings include Gitlab::GonHelper include PageLayoutHelper + include OauthApplications before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! before_action :add_gon_variables + before_action :load_scopes, only: [:index, :create, :edit] layout 'profile' diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 508b82a9a6c2c582fc9f781e3cc09aa088c89d38..6e007f17913350b0f92955218ffc2f93a4f2c25b 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -1,8 +1,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController - before_action :load_personal_access_tokens, only: :index - def index - @personal_access_token = current_user.personal_access_tokens.build + set_index_vars end def create @@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController flash[:personal_access_token] = @personal_access_token.token redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." else - load_personal_access_tokens + set_index_vars render :index end end @@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController private def personal_access_token_params - params.require(:personal_access_token).permit(:name, :expires_at) + params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) end - def load_personal_access_tokens + def set_index_vars + @personal_access_token ||= current_user.personal_access_tokens.build + @scopes = Gitlab::Auth::SCOPES @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at) @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 9eb75bb389185e5e4442882681ca46749434151f..18044ca78e2074285027ce2ba6babec795d786ca 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end @qr_code = build_qr_code + @account_string = account_string setup_u2f_registration end @@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController private def build_qr_code - issuer = "#{issuer_host} | #{current_user.email}" - uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host) RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) end + def account_string + "#{issuer_host}:#{current_user.email}" + end + def issuer_host Gitlab.config.gitlab.host end diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9dfa53466981896e96e495d295fff7452934cda --- /dev/null +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -0,0 +1,48 @@ +class Projects::AutocompleteSourcesController < Projects::ApplicationController + before_action :load_autocomplete_service, except: [:emojis, :members] + + def emojis + render json: Gitlab::AwardEmoji.urls + end + + def members + render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) + end + + def issues + render json: @autocomplete_service.issues + end + + def merge_requests + render json: @autocomplete_service.merge_requests + end + + def labels + render json: @autocomplete_service.labels + end + + def milestones + render json: @autocomplete_service.milestones + end + + def commands + render json: @autocomplete_service.commands(noteable, params[:type]) + end + + private + + def load_autocomplete_service + @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user) + end + + def noteable + case params[:type] + when 'Issue' + IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + when 'MergeRequest' + MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + when 'Commit' + @project.commit(params[:type_id]) + end + end +end diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index f576d0be1fc099e3c16c4419b157e3817b1e8fcf..863a766a2552cb3ee18e11d4c03c712cfb8a19de 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -8,6 +8,9 @@ class Projects::BlameController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) + + return render_404 unless @blob + @blame_groups = Gitlab::Blame.new(@blob, @commit).groups end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 8197d9e4c9929ea40749c99ef7693a3c0f107326..791ed88db30faee34999024814cb12ab7c42103e 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -8,13 +8,11 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds] - before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] + before_action :authorize_download_code! before_action :authorize_read_pipeline!, only: [:pipelines] - before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines] - before_action :define_status_vars, only: [:show, :builds, :pipelines] + before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines] + before_action :define_status_vars, only: [:show, :pipelines] before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] @@ -35,25 +33,6 @@ class Projects::CommitController < Projects::ApplicationController def pipelines end - def builds - end - - def cancel_builds - ci_builds.running_or_pending.each(&:cancel) - - redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) - end - - def retry_builds - ci_builds.latest.failed.each do |build| - if build.retryable? - Ci::Build.retry(build, current_user) - end - end - - redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) - end - def branches @branches = @project.repository.branch_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id) @@ -98,10 +77,6 @@ class Projects::CommitController < Projects::ApplicationController @noteable = @commit ||= @project.commit(params[:id]) end - def ci_builds - @ci_builds ||= Ci::Build.where(pipeline: pipelines) - end - def define_commit_vars return git_not_found! unless commit @@ -133,8 +108,6 @@ class Projects::CommitController < Projects::ApplicationController def define_status_vars @ci_pipelines = project.pipelines.where(sha: commit.sha) - @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant - @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index ec02fc15d35b9ce695cc6e69cb917b53612be435..d32966645c85bbe253248e3fb53930b996106fca 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController end def create - redirect_to namespace_project_compare_path(@project.namespace, @project, + if params[:from].blank? || params[:to].blank? + flash[:alert] = "You must select from and to branches" + from_to_vars = { + from: params[:from].presence, + to: params[:to].presence + } + redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars) + else + redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) + end end private diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 13b3eec761fa52645d92aa1abd88fedccbf83659..b69d46f2c41acf0966316243c25865e9cb71c9a7 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -9,56 +9,52 @@ module Projects before_action :authorize_read_merge_request!, only: [:code, :review] def issue - render_events(events.issue_events) + render_events(cycle_analytics[:issue].events) end def plan - render_events(events.plan_events) + render_events(cycle_analytics[:plan].events) end def code - render_events(events.code_events) + render_events(cycle_analytics[:code].events) end def test - options[:branch] = events_params[:branch_name] + options(events_params)[:branch] = events_params[:branch_name] - render_events(events.test_events) + render_events(cycle_analytics[:test].events) end def review - render_events(events.review_events) + render_events(cycle_analytics[:review].events) end def staging - render_events(events.staging_events) + render_events(cycle_analytics[:staging].events) end def production - render_events(events.production_events) + render_events(cycle_analytics[:production].events) end private - - def render_events(events_list) + + def render_events(events) respond_to do |format| format.html - format.json { render json: { events: events_list } } + format.json { render json: { events: events } } end end - def events - @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options) - end - - def options - @options ||= { from: start_date(events_params), current_user: current_user } + def cycle_analytics + @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end def events_params return {} unless params[:events].present? - params[:events].slice(:start_date, :branch_name) + params[:events].permit(:start_date, :branch_name) end end end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index ac639ef015b546fdd16c86fb37efed8a3d47d90e..88ac3ad046b57f223c930fd20b45147543d68abb 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params)) - stats_values, cycle_analytics_json = generate_cycle_analytics_data - - @cycle_analytics_no_data = stats_values.blank? + @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| format.html @@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def cycle_analytics_params return {} unless params[:cycle_analytics].present? - { start_date: params[:cycle_analytics][:start_date] } + params[:cycle_analytics].permit(:start_date) end - def generate_cycle_analytics_data - stats_values = [] - - cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"], - [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"], - [:code, "Code", "Related Merge Requests", "Time spent coding"], - [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"], - [:review, "Review", "Relative Merged Requests", "The time taken to review the code"], - [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"], - [:production, "Production", "Related Issues", "The total time taken from idea to production"]] - - stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)| - value = @cycle_analytics.send(stage_method).presence - - stats_values << value.abs if value - - stats << { - title: stage_text, - description: stage_description, - legend: stage_legend, - value: value && !value.zero? ? distance_of_time_in_words(value) : nil - } - - stats - end - - issues = @cycle_analytics.summary.new_issues - commits = @cycle_analytics.summary.commits - deploys = @cycle_analytics.summary.deploys - - summary = [ - { title: "New Issue".pluralize(issues), value: issues }, - { title: "Commit".pluralize(commits), value: commits }, - { title: "Deploy".pluralize(deploys), value: deploys } - ] - - cycle_analytics_hash = { summary: summary, - stats: stats, - permissions: @cycle_analytics.permissions(user: current_user) + def cycle_analytics_json + { + summary: @cycle_analytics.summary, + stats: @cycle_analytics.stats, + permissions: @cycle_analytics.permissions(user: current_user) } - - [stats_values, cycle_analytics_hash] end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 529e0aa2d33e13c96037fc8f6e2b05e40608ad6a..b094491e006705d9a724613932c787eca57f99c4 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create - @key = DeployKey.new(deploy_key_params) + @key = DeployKey.new(deploy_key_params.merge(user: current_user)) set_index_vars if @key.valid? && @project.deploy_keys << @key @@ -53,6 +53,6 @@ class Projects::DeployKeysController < Projects::ApplicationController end def deploy_key_params - params.require(:deploy_key).permit(:key, :title) + params.require(:deploy_key).permit(:key, :title, :can_push) end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 6bd4cb3f2f5886b1e0ce2fe55b62beeadd5badfd..87cc36253f1a209b8b9bdd2e3667b3a5561f7064 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -4,17 +4,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] - before_action :environment, only: [:show, :edit, :update, :stop] + before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :verify_api_request!, only: :terminal_websocket_authorize def index @scope = params[:scope] @environments = project.environments - + respond_to do |format| format.html format.json do render json: EnvironmentSerializer - .new(project: @project) + .new(project: @project, user: current_user) .represent(@environments) end end @@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) end + def terminal + # Currently, this acts as a hint to load the terminal details into the cache + # if they aren't there already. In the future, users will need these details + # to choose between terminals to connect to. + @terminals = environment.terminals + end + + # GET .../terminal.ws : implemented in gitlab-workhorse + def terminal_websocket_authorize + # Just return the first terminal for now. If the list is in the process of + # being looked up, this may result in a 404 response, so the frontend + # should retry those errors + terminal = environment.terminals.try(:first) + if terminal + set_workhorse_internal_api_content_type + render json: Gitlab::Workhorse.terminal_websocket(terminal) + else + render text: 'Not found', status: 404 + end + end + private + def verify_api_request! + Gitlab::Workhorse.verify_api_request!(request.headers) + end + def environment_params params.require(:environment).permit(:name, :external_url) end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 9eaf26a0dbf966743dcfe1998824b94771491313..66b7bdbd9889bec4f5e44180a661e32cf26b7938 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController before_action :authorize_admin_project_member!, only: [:update] def index - @group_links = project.project_group_links.all - - @skip_groups = @group_links.pluck(:group_id) - @skip_groups << project.namespace_id unless project.personal? + redirect_to namespace_project_settings_members_path end def create @@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController flash[:alert] = 'Please select a group.' end - redirect_to namespace_project_group_links_path(project.namespace, project) + redirect_to namespace_project_settings_members_path(project.namespace, project) end def update @@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to namespace_project_group_links_path(project.namespace, project) + redirect_to namespace_project_settings_members_path(project.namespace, project) end format.js { head :ok } end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4f66e01e0f7a9ca6c8eaff3460fbff2ba87c8772..2beb0df8a07fd1b259f61111ae1d4d7469488897 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -25,6 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController def index @issues = issues_collection @issues = @issues.page(params[:page]) + if @issues.out_of_range? && @issues.total_pages != 0 + return redirect_to url_for(params.merge(page: @issues.total_pages)) + end if params[:label_name].present? @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..01d99c7df3504b18a276823eda3cafa2b8872cf2 --- /dev/null +++ b/app/controllers/projects/mattermosts_controller.rb @@ -0,0 +1,43 @@ +class Projects::MattermostsController < Projects::ApplicationController + include TriggersHelper + include ActionView::Helpers::AssetUrlHelper + + layout 'project_settings' + + before_action :authorize_admin_project! + before_action :service + before_action :teams, only: [:new] + + def new + end + + def create + result, message = @service.configure(current_user, configure_params) + + if result + flash[:notice] = 'This service is now configured' + redirect_to edit_namespace_project_service_path( + @project.namespace, @project, service) + else + flash[:alert] = message || 'Failed to configure service' + redirect_to new_namespace_project_mattermost_path( + @project.namespace, @project) + end + end + + private + + def configure_params + params.require(:mattermost).permit(:trigger, :team_id).merge( + url: service_trigger_url(@service), + icon_url: asset_url('slash-command-logo.png')) + end + + def teams + @teams ||= @service.list_teams(current_user) + end + + def service + @service ||= @project.find_or_initialize_service('mattermost_slash_commands') + end +end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f0cb5a9d4b4e4ca1ee4ffe5ef1c2eec9c540d6ac..9ac5bf4b9f8313632ce558023a8f35eab648ab3d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,10 +9,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] @@ -38,6 +38,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) + if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 + return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) + end if params[:label_name].present? labels_params = { project_id: @project.id, title: params[:label_name] } @@ -95,7 +98,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } unless @start_version - render_404 + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff end end @@ -201,17 +205,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def builds - respond_to do |format| - 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 pipelines @pipelines = @merge_request.all_pipelines @@ -354,6 +347,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def merge_widget_refresh + if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' + @status = :success + elsif merge_request.merge_when_build_succeeds + @status = :merge_when_build_succeeds + end + + render 'merge' + end + def branch_from # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project @@ -416,10 +419,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController else ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service - - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch) - end end response = { diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 15ca080c6964d616a05614e61ef0740cf99ecbbd..c5d93ce25bcb1a65c0ba8fbdb3b7834ccbd878ad 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController end def create - @note = Notes::CreateService.new(project, current_user, note_params).execute + create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha]) + @note = Notes::CreateService.new(project, current_user, create_params).execute if @note.is_a?(Note) Banzai::NoteRenderer.render([@note], @project, current_user) @@ -217,6 +218,6 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new.execute(project, current_user, params) + @notes = NotesFinder.new(project, current_user, params).execute.inc_author end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 85188cfdd4ca7e597da0bdc4d2de33ca564aee72..84451257b98f898946e27f68af46b40ad3f9df9f 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -7,10 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] - @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) + @pipelines = PipelinesFinder + .new(project) + .execute(scope: @scope) + .page(params[:page]) + .per(30) - @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count - @pipelines_count = PipelinesFinder.new(project).execute.count + @running_or_pending_count = PipelinesFinder + .new(project).execute(scope: 'running').count + + @pipelines_count = PipelinesFinder + .new(project).execute.count + + respond_to do |format| + format.html + format.json do + render json: { + pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines), + count: { + all: @pipelines_count, + running_or_pending: @running_or_pending_count + } + } + end + end end def new @@ -40,6 +63,15 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def stage + @stage = pipeline.stage(params[:stage]) + return not_found unless @stage + + respond_to do |format| + format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } } + end + end + def retry pipeline.retry_failed(current_user) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 53308948f623ce6bdfc605ed08fc37212f94f6ea..6e158e685e99470e5d6b1203266732f5c0b4c819 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,56 +1,19 @@ class Projects::ProjectMembersController < Projects::ApplicationController include MembershipActions + include SortingHelper # Authorize before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index - @group_links = @project.project_group_links - - @project_members = @project.project_members - @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) - - group = @project.group - - if group - # We need `.where.not(user_id: nil)` here otherwise when a group has an - # invitee, it would make the following query return 0 rows since a NULL - # user_id would be present in the subquery - # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values - # FIXME: This whole logic should be moved to a finder! - non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) - group_members = group.group_members.where.not(user_id: non_null_user_ids) - group_members = group_members.non_invite unless can?(current_user, :admin_group, @group) - end - - if params[:search].present? - user_ids = @project.users.search(params[:search]).select(:id) - @project_members = @project_members.where(user_id: user_ids) - - if group_members - user_ids = group.users.search(params[:search]).select(:id) - group_members = group_members.where(user_id: user_ids) - end - - @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) - end - - wheres = ["id IN (#{@project_members.select(:id).to_sql})"] - wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members - - @project_members = Member. - where(wheres.join(' OR ')). - order(access_level: :desc).page(params[:page]) - - @requesters = AccessRequestsFinder.new(@project).execute(current_user) - - @project_member = @project.project_members.new + sort = params[:sort].presence || sort_value_name + redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort) end def create status = Members::CreateService.new(@project, current_user, params).execute - redirect_url = namespace_project_project_members_path(@project.namespace, @project) + redirect_url = namespace_project_settings_members_path(@project.namespace, @project) if status redirect_to redirect_url, notice: 'Users were successfully added.' @@ -73,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to namespace_project_project_members_path(@project.namespace, @project) + redirect_to namespace_project_settings_members_path(@project.namespace, @project) end format.js { head :ok } end end def resend_invite - redirect_path = namespace_project_project_members_path(@project.namespace, @project) + redirect_path = namespace_project_settings_members_path(@project.namespace, @project) @project_member = @project.project_members.find(params[:id]) @@ -103,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController return render_404 end - redirect_to(namespace_project_project_members_path(project.namespace, project), + redirect_to(namespace_project_settings_members_path(project.namespace, project), notice: notice) end diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..5735e281f66e772b5b31600ef71e777a233d94d6 --- /dev/null +++ b/app/controllers/projects/settings/members_controller.rb @@ -0,0 +1,55 @@ +module Projects + module Settings + class MembersController < Projects::ApplicationController + include SortingHelper + + def show + @sort = params[:sort].presence || sort_value_name + @group_links = @project.project_group_links + + @project_members = @project.project_members + @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + + group = @project.group + + # group links + @group_links = @project.project_group_links.all + + @skip_groups = @group_links.pluck(:group_id) + @skip_groups << @project.namespace_id unless @project.personal? + + if group + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + group_members = MembersFinder.new(@project_members, group).execute(current_user) + end + + if params[:search].present? + user_ids = @project.users.search(params[:search]).select(:id) + @project_members = @project_members.where(user_id: user_ids) + + if group_members + user_ids = group.users.search(params[:search]).select(:id) + group_members = group_members.where(user_id: user_ids) + end + + @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) + end + + wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] + wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members + + @project_members = Member. + where(wheres.join(' OR ')). + sort(@sort). + page(params[:page]) + + @requesters = AccessRequestsFinder.new(@project).execute(current_user) + + @project_member = @project.project_members.new + end + end + end +end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 0720be2e55d15ea3dd17e68e56476f2897d83933..02a97c1c574ccfa73d235958b4fc87793dd13ea0 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -26,6 +26,9 @@ class Projects::SnippetsController < Projects::ApplicationController scope: params[:scope] ) @snippets = @snippets.page(params[:page]) + if @snippets.out_of_range? && @snippets.total_pages != 0 + redirect_to namespace_project_snippets_path(page: @snippets.total_pages) + end end def new diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 953091492aeb8ede5892f8836a61594a5e93c431..e2d9d5ed4606e1bafe89cd7bc96bb2e6ab2ebf8d 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def index - params[:sort] = params[:sort].presence || 'name' + params[:sort] = params[:sort].presence || sort_value_recently_updated @sort = params[:sort] @tags = TagsFinder.new(@repository, params).execute diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a8a18b4fa1673c7a48a9c6fd8aee27f564f12cca..444ff837bb379a872a5387aae54da20180aa8a34 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController end def update - status = ::Projects::UpdateService.new(@project, current_user, project_params).execute + result = ::Projects::UpdateService.new(@project, current_user, project_params).execute # Refresh the repo in case anything changed - @repository = project.repository + @repository = @project.repository respond_to do |format| - if status + if result[:status] == :success flash[:notice] = "Project '#{@project.name}' was successfully updated." format.html do - redirect_to( - edit_project_path(@project), - notice: "Project '#{@project.name}' was successfully updated." - ) + redirect_to(edit_project_path(@project)) end else format.html { render 'edit' } @@ -127,39 +124,6 @@ class ProjectsController < Projects::ApplicationController redirect_to edit_project_path(@project), alert: ex.message end - def autocomplete_sources - noteable = - case params[:type] - when 'Issue' - IssuesFinder.new(current_user, project_id: @project.id). - execute.find_by(iid: params[:type_id]) - when 'MergeRequest' - MergeRequestsFinder.new(current_user, project_id: @project.id). - execute.find_by(iid: params[:type_id]) - when 'Commit' - @project.commit(params[:type_id]) - else - nil - end - - autocomplete = ::Projects::AutocompleteService.new(@project, current_user) - participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) - - @suggestions = { - emojis: Gitlab::AwardEmoji.urls, - issues: autocomplete.issues, - milestones: autocomplete.milestones, - mergerequests: autocomplete.merge_requests, - labels: autocomplete.labels, - members: participants, - commands: autocomplete.commands(noteable, params[:type]) - } - - respond_to do |format| - format.json { render json: @suggestions } - end - end - def new_issue_address return render_404 unless Gitlab::IncomingEmail.supports_issue_creation? diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c45196cc3e91595150bc5b26a74311d23b007ecb..bf27f3d4d514547b5e530e41efcc6d8d732843c3 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -7,17 +7,17 @@ class RegistrationsController < Devise::RegistrationsController end def create - if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha - # To avoid duplicate form fields on the login page, the registration form - # names fields using `new_user`, but Devise still wants the params in - # `user`. - if params["new_#{resource_name}"].present? && params[resource_name].blank? - params[resource_name] = params.delete(:"new_#{resource_name}") - end + # To avoid duplicate form fields on the login page, the registration form + # names fields using `new_user`, but Devise still wants the params in + # `user`. + if params["new_#{resource_name}"].present? && params[resource_name].blank? + params[resource_name] = params.delete(:"new_#{resource_name}") + end + if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha super else - flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." + flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' flash.delete :recaptcha_error render action: 'new' end @@ -57,7 +57,7 @@ class RegistrationsController < Devise::RegistrationsController end def sign_up_params - params.require(:user).permit(:username, :email, :name, :password, :password_confirmation) + params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) end def resource_name diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8c698695202681347dcc3e332a3df2c1929a7f4d..93a180b903609f2e42246f53ce30d8bf56d98e7b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -114,7 +114,7 @@ class SessionsController < Devise::SessionsController def valid_otp_attempt?(user) user.validate_and_consume_otp!(user_params[:otp_attempt]) || - user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end def log_audit_event(user, options = {}) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b4c14d05eaf989a3a90fb7cfb4406d9698991faa..1576fc80a6b3b02a64571802765c0233a34898bd 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -165,31 +165,53 @@ class IssuableFinder end end - def assignee? - params[:assignee_id].present? + def assignee_id? + params[:assignee_id].present? && params[:assignee_id] != NONE + end + + def assignee_username? + params[:assignee_username].present? && params[:assignee_username] != NONE + end + + def no_assignee? + # Assignee_id takes precedence over assignee_username + params[:assignee_id] == NONE || params[:assignee_username] == NONE end def assignee return @assignee if defined?(@assignee) @assignee = - if assignee? && params[:assignee_id] != NONE - User.find(params[:assignee_id]) + if assignee_id? + User.find_by(id: params[:assignee_id]) + elsif assignee_username? + User.find_by(username: params[:assignee_username]) else nil end end - def author? - params[:author_id].present? + def author_id? + params[:author_id].present? && params[:author_id] != NONE + end + + def author_username? + params[:author_username].present? && params[:author_username] != NONE + end + + def no_author? + # author_id takes precedence over author_username + params[:author_id] == NONE || params[:author_username] == NONE end def author return @author if defined?(@author) @author = - if author? && params[:author_id] != NONE - User.find(params[:author_id]) + if author_id? + User.find_by(id: params[:author_id]) + elsif author_username? + User.find_by(username: params[:author_username]) else nil end @@ -263,16 +285,24 @@ class IssuableFinder end def by_assignee(items) - if assignee? - items = items.where(assignee_id: assignee.try(:id)) + if assignee + items = items.where(assignee_id: assignee.id) + elsif no_assignee? + items = items.where(assignee_id: nil) + elsif assignee_id? || assignee_username? # assignee not found + items = items.none end items end def by_author(items) - if author? - items = items.where(author_id: author.try(:id)) + if author + items = items.where(author_id: author.id) + elsif no_author? + items = items.where(author_id: nil) + elsif author_id? || author_username? # author not found + items = items.none end items diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index be00a219205de287be0729417a12374b63cfc682..707eddd4d2991138ee6fb9d2f279e25e8ba48416 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder private def init_collection - Issue.visible_to_user(current_user) + IssuesFinder.not_restricted_by_confidentiality(current_user) end def iid_pattern @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} end + + def self.not_restricted_by_confidentiality(user) + return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? + + return Issue.all if user.admin? + + Issue.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: user.id, + project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) + end end diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..702944404f50909d3fcb37f1ea5eb2b440f9c1fd --- /dev/null +++ b/app/finders/members_finder.rb @@ -0,0 +1,13 @@ +class MembersFinder < Projects::ApplicationController + def initialize(project_members, project_group) + @project_members = project_members + @project_group = project_group + end + + def execute(current_user) + non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) + group_members = @project_group.group_members.where.not(user_id: non_null_user_ids) + group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group) + group_members + end +end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 2484339e3a4cf2bc66a4a079c4db9d6c1cfb8956..4bd8c83081a479238298b8554dca3a876710a93c 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,27 +1,102 @@ class NotesFinder FETCH_OVERLAP = 5.seconds - def execute(project, current_user, params) - target_type = params[:target_type] - target_id = params[:target_id] - # Default to 0 to remain compatible with old clients - last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) - - notes = - case target_type - when "commit" - project.notes.for_commit_id(target_id).non_diff_notes - when "issue" - IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author - when "merge_request" - MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author - when "snippet", "project_snippet" - project.snippets.find(target_id).notes + # Used to filter Notes + # When used with target_type and target_id this returns notes specifically for the controller + # + # Arguments: + # current_user - which user check authorizations with + # project - which project to look for notes on + # params: + # target_type: string + # target_id: integer + # last_fetched_at: time + # search: string + # + def initialize(project, current_user, params = {}) + @project = project + @current_user = current_user + @params = params + init_collection + end + + def execute + @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at] + @notes + end + + private + + def init_collection + if @params[:target_id] + @notes = on_target(@params[:target_type], @params[:target_id]) + else + @notes = notes_of_any_type + end + end + + def notes_of_any_type + types = %w(commit issue merge_request snippet) + note_relations = types.map { |t| notes_for_type(t) } + note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search] + UnionFinder.new.find_union(note_relations, Note) + end + + def noteables_for_type(noteable_type) + case noteable_type + when "issue" + IssuesFinder.new(@current_user, project_id: @project.id).execute + when "merge_request" + MergeRequestsFinder.new(@current_user, project_id: @project.id).execute + when "snippet", "project_snippet" + SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + else + raise 'invalid target_type' + end + end + + def notes_for_type(noteable_type) + if noteable_type == "commit" + if Ability.allowed?(@current_user, :download_code, @project) + @project.notes.where(noteable_type: 'Commit') + else + Note.none + end + else + finder = noteables_for_type(noteable_type) + @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil)) + end + end + + def on_target(target_type, target_id) + if target_type == "commit" + notes_for_type('commit').for_commit_id(target_id) + else + target = noteables_for_type(target_type).find(target_id) + + if target.respond_to?(:related_notes) + target.related_notes else - raise 'invalid target_type' + target.notes end + end + end + + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + def search(query, notes_relation = @notes) + pattern = "%#{query}%" + notes_relation.where(Note.arel_table[:note].matches(pattern)) + end + + # Notes changed since last fetch + # Uses overlapping intervals to avoid worrying about race conditions + def since_fetch_at(fetch_time) + # Default to 0 to remain compatible with old clients + last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) - # Use overlapping intervals to avoid worrying about race conditions - notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh + @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c816b616631c7992513bb3399bf93d383206fb86..a112928c6dedfb94d551c464d6aa6cd1c0a6a568 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -244,7 +244,9 @@ module ApplicationHelper scope: params[:scope], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], + assignee_username: params[:assignee_username], author_id: params[:author_id], + author_username: params[:author_username], search: params[:search], label_name: params[:label_name] } diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 92bac149313d00d6cefaeffc05cf563e17618aae..1ee6c1d3afa73cd2e87ae38679c81ee91fe1bdd4 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 07ff6fb94889c58503a881861c64f31d3a18f805..c3508443d8aeff693c5aa09d862819827655757f 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -188,7 +188,11 @@ module BlobHelper end def gitlab_ci_ymls - @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names + @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) + end + + def dockerfile_names + @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names end def blob_editor_paths diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e19752a8a1142565450fe635345ca277e52cd70..94f3b48017814cf8d74ab363ed585d3d4ef7187e 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,28 +1,10 @@ module CiStatusHelper def ci_status_path(pipeline) project = pipeline.project - builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) - end - - def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_text_for_status(status) - klass = "ci-status ci-#{status}" - - if target - link_to content, target, class: klass - else - content_tag :span, content, class: klass - end - end - - def ci_text_for_status(status) - if detailed_status?(status) - status.text - else - status - end + namespace_project_pipeline_path(project.namespace, project, pipeline) end + # Is used by Commit and Merge Request Widget def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 66a720a9426e890b87a590f0bd682a22a3cadb07..e9461b9f85923041dcabe75f2bb637d1b0d523cf 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,50 +128,11 @@ module CommitsHelper end def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - - if can_collaborate_with_project? - btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to revert this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - btn_class = "btn btn-grouped btn-warning" unless btn_class.nil? - - link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" - - if can_collaborate_with_project? - btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? - link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - btn_class = "btn btn-grouped btn-close" unless btn_class.nil? - link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end protected @@ -211,6 +172,28 @@ module CommitsHelper end end + def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true) + return unless current_user + + tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip + btn_class = "btn btn-#{btn_class}" unless btn_class.nil? + + if can_collaborate_with_project? + link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.", + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) + end + end + def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index c35d6611ab05b02dd556e051951c8ba8f3c0d9b9..aed1d7c839faa28cd6469b2f7183459704bf6527 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -165,4 +165,10 @@ module DiffHelper link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] end + + def render_overflow_warning?(diff_files) + diffs = @merge_request_diff.presence || diff_files + + diffs.overflow? + end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 6a43be2cf3ef14049e07f6b38ed9a4ab05e0fe13..1182939f656958db73c42a4673a0b1a4304e5ba8 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -7,12 +7,12 @@ module FormHelper content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << - content_tag(:ul) do - model.errors.full_messages. - map { |msg| content_tag(:li, msg) }. - join. - html_safe - end + content_tag(:ul) do + model.errors.full_messages. + map { |msg| content_tag(:li, msg) }. + join. + html_safe + end end end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index eb435cc178374021f407dffcc70f0e599cdcf1d0..6d365ea925140029405e5ce009d8c8406c314f13 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -110,6 +110,28 @@ module GitlabMarkdownHelper end end + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String + def cross_project_reference(project, entity) + if entity.respond_to?(:to_reference) + entity.to_reference(project, full: true) + else + '' + end + end + private # Return +text+, truncated to +max_chars+ characters, excluding any HTML @@ -158,28 +180,6 @@ module GitlabMarkdownHelper end end - # Returns the text necessary to reference `entity` across projects - # - # project - Project to reference - # entity - Object that responds to `to_reference` - # - # Examples: - # - # cross_project_reference(project, project.issues.first) - # # => 'namespace1/project1#123' - # - # cross_project_reference(project, project.merge_requests.first) - # # => 'namespace1/project1!345' - # - # Returns a String - def cross_project_reference(project, entity) - if entity.respond_to?(:to_reference) - entity.to_reference(project) - else - '' - end - end - def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: "body" }) content_tag :button, diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 99db73c9ee04b2778e922140cf3fbb9266afc47b..5742fec44581f21ce79bd5ecb8002ebdf41a59ff 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -206,4 +206,9 @@ module GitlabRoutingHelper file_namespace_project_build_artifacts_path(*args) end end + + # Settings + def project_settings_members_path(project, *args) + namespace_project_settings_members_path(project.namespace, project, *args) + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index f6d4ea4659a56e50bad14857f332d86249ba6ed1..77dc9e7d538dab25c78f13c502b0c934fcee6482 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,11 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) - full_title = link_to(simple_sanitize(group.name), group_path(group)) + full_title = '' + + group.parents.each do |parent| + full_title += link_to(simple_sanitize(parent.name), group_path(parent)) + full_title += ' / '.html_safe + end + + full_title += link_to(simple_sanitize(group.name), group_path(group)) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name content_tag :span do - full_title + full_title.html_safe end end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 021d2b1471826dff1982c2784900ff13aefed4da..a0642a1894b6413b8c4ef7d52f416f32729390bb 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -4,8 +4,10 @@ module ImportHelper "#{namespace}/#{name}" end - def github_project_link(path_with_namespace) - link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' + def provider_project_link(provider, path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) + + link_to path_with_namespace, url, target: '_blank' end private @@ -20,4 +22,8 @@ module ImportHelper provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } @github_url = provider.fetch('url', 'https://github.com') if provider end + + def gitea_project_url(path_with_namespace) + "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8231f8fa334b871d419eac3c7ef78e6ec6bc0f6d..e5bb8b93e7693e5d096949f66d855a6a0a5ecbd5 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -30,6 +30,15 @@ module IssuablesHelper end end + def serialize_issuable(issuable) + case issuable + when Issue + IssueSerializer.new.represent(issuable).to_json + when MergeRequest + MergeRequestSerializer.new.represent(issuable).to_json + end + end + def template_dropdown_tag(issuable, &block) title = selected_template(issuable) || "Choose a template" options = { @@ -96,8 +105,8 @@ module IssuablesHelper if issuable.tasks? output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg") + output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") end output diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index a8a49e43b05ed270097456748b5c33501f519b91..a2d21b67a775dae1822aa176594eaf7161588359 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -58,13 +58,13 @@ module IssuesHelper end def status_box_class(item) - if item.respond_to?(:expired?) && item.expired? + if item.try(:expired?) 'status-box-expired' - elsif item.respond_to?(:merged?) && item.merged? + elsif item.try(:merged?) 'status-box-merged' elsif item.closed? 'status-box-closed' - elsif item.respond_to?(:upcoming?) && item.upcoming? + elsif item.try(:upcoming?) 'status-box-upcoming' else 'status-box-open' @@ -128,8 +128,10 @@ module IssuesHelper names.to_sentence end - def award_active_class(awards, current_user) - if current_user && awards.find { |a| a.user_id == current_user.id } + def award_state_class(awards, current_user) + if !current_user + "disabled" + elsif current_user && awards.find { |a| a.user_id == current_user.id } "active" else "" diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..49ac12db832827dc1e4561be3691298aee0b2754 --- /dev/null +++ b/app/helpers/mattermost_helper.rb @@ -0,0 +1,9 @@ +module MattermostHelper + def mattermost_teams_options(teams) + teams_options = teams.map do |id, options| + [options['display_name'] || options['name'], id] + end + + teams_options.compact.unshift(['Select team...', '0']) + end +end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 877c77050bed0979d2814dacb549fd279f3d89e0..41d471cc92f973bad92e6cb74d784e88f706f7ae 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -36,4 +36,12 @@ module MembersHelper "Are you sure you want to leave the " \ "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?" end + + def filter_group_project_member_path(options = {}) + options = params.slice(:search, :sort).merge(options) + + path = request.path + path << "?#{options.to_param}" + path + end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index a6659ea2fd1d0806d5da53b4bc7b7c51561e8df2..8c2c4e8833ba8e8cfdbd6a81067ac5b7ab98b2b9 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -19,6 +19,14 @@ module MergeRequestsHelper } end + def mr_widget_refresh_url(mr) + if mr && mr.source_project + merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr) + else + '' + end + end + def mr_css_classes(mr) classes = "merge-request" classes << " closed" if mr.closed? @@ -59,6 +67,10 @@ module MergeRequestsHelper @mr_closes_issues ||= @merge_request.closes_issues end + def mr_issues_mentioned_but_not_closing + @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing + end + def mr_change_branches_path(merge_request) new_namespace_project_merge_request_path( @project.namespace, @project, diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a3331dc80cb5f91c5e25f8812880328c005b2b6a..e21178c7377139fa37ea4059f3b05aac52144e09 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -7,12 +7,12 @@ module NavHelper def page_gutter_class if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || - current_path?('merge_requests#builds') || - current_path?('merge_requests#conflicts') || - current_path?('merge_requests#pipelines') || - current_path?('issues#show') + current_path?('merge_requests#diffs') || + current_path?('merge_requests#commits') || + current_path?('merge_requests#builds') || + current_path?('merge_requests#conflicts') || + current_path?('merge_requests#pipelines') || + current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" else @@ -21,9 +21,9 @@ module NavHelper elsif current_path?('builds#show') "page-gutter build-sidebar right-sidebar-expanded" elsif current_path?('wikis#show') || - current_path?('wikis#edit') || - current_path?('wikis#history') || - current_path?('wikis#git_access') + current_path?('wikis#edit') || + current_path?('wikis#history') || + current_path?('wikis#git_access') "page-gutter wiki-sidebar right-sidebar-expanded" end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9cda3b787613db6f04db163c788dc47cba16c40e..c6c63918fa5b44cc57e128feeb42dd99b6bc2f2e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper def project_title(project) namespace_link = if project.group - link_to(simple_sanitize(project.group.name), group_path(project.group)) + group_title(project.group) else owner = project.namespace.owner link_to(simple_sanitize(owner.name), user_path(owner)) @@ -61,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 << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do + project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do icon("chevron-down") end end @@ -90,10 +90,12 @@ module ProjectsHelper end def project_for_deploy_key(deploy_key) - if deploy_key.projects.include?(@project) + if deploy_key.has_access_to?(@project) @project else - deploy_key.projects.find { |project| can?(current_user, :read_project, project) } + deploy_key.projects.find do |project| + can?(current_user, :read_project, project) + end end end @@ -171,48 +173,27 @@ module ProjectsHelper nav_tabs << :merge_requests end - if can?(current_user, :read_pipeline, project) - nav_tabs << :pipelines - end - - if can?(current_user, :read_build, project) - nav_tabs << :builds - end - if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) nav_tabs << :container_registry end - if can?(current_user, :read_environment, project) - nav_tabs << :environments - end - - if can?(current_user, :admin_project, project) - nav_tabs << :settings - end - - if can?(current_user, :read_project_member, project) - nav_tabs << :team - end - - if can?(current_user, :read_issue, project) - nav_tabs << :issues - end - - if can?(current_user, :read_wiki, project) - nav_tabs << :wiki - end - - if can?(current_user, :read_project_snippet, project) - nav_tabs << :snippets - end - - if can?(current_user, :read_label, project) - nav_tabs << :labels - end + tab_ability_map = { + environments: :read_environment, + milestones: :read_milestone, + pipelines: :read_pipeline, + snippets: :read_project_snippet, + settings: :admin_project, + builds: :read_build, + labels: :read_label, + issues: :read_issue, + team: :read_project_member, + wiki: :read_wiki + } - if can?(current_user, :read_milestone, project) - nav_tabs << :milestones + tab_ability_map.each do |tab, ability| + if can?(current_user, ability, project) + nav_tabs << tab + end end nav_tabs.flatten @@ -246,11 +227,6 @@ module ProjectsHelper end end - def repository_size(project = @project) - size_in_bytes = project.repository_size * 1.megabyte - number_to_human_size(size_in_bytes, delimiter: ',', precision: 2) - end - def default_url_to_repo(project = @project) case default_clone_protocol when 'ssh' @@ -280,13 +256,15 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil) + def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil) namespace_project_new_blob_path( project.namespace, project, project.default_branch || 'master', file_name: file_name, - commit_message: commit_message || "Add #{file_name.downcase}" + commit_message: commit_message || "Add #{file_name.downcase}", + target_branch: target_branch, + context: context ) end @@ -390,26 +368,12 @@ module ProjectsHelper "success" end end - + def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') end - def round_commit_count(project) - count = project.commit_count - - if count > 10000 - '10000+' - elsif count > 5000 - '5000+' - elsif count > 1000 - '1000+' - else - count - end - end - def current_ref @ref || @repository.try(:root_ref) end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cdb9663877cd37b0a2d994c24d6c7a2956f39e1c..6654f6997ce2c10c47a0417067f84506a1610ea6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -39,7 +39,7 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "Dashboard", url: root_path }, { category: "Settings", label: "Admin Section", url: admin_root_path }, @@ -75,7 +75,7 @@ module SearchHelper { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) }, { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 8b138a8e69f6d4015fcf415b1dad7e68f07616c7..ff787fb4131480c8b958a39b82acc1d54b6c0bc6 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -11,6 +11,7 @@ module SortingHelper sort_value_due_date_soon => sort_title_due_date_soon, sort_value_due_date_later => sort_title_due_date_later, sort_value_largest_repo => sort_title_largest_repo, + sort_value_largest_group => sort_title_largest_group, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, sort_value_downvotes => sort_title_downvotes, @@ -25,7 +26,7 @@ module SortingHelper sort_value_recently_updated => sort_title_recently_updated, sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_created => sort_title_oldest_created } if current_controller?('admin/projects') @@ -35,6 +36,19 @@ module SortingHelper options end + def member_sort_options_hash + { + sort_value_access_level_asc => sort_title_access_level_asc, + sort_value_access_level_desc => sort_title_access_level_desc, + sort_value_last_joined => sort_title_last_joined, + sort_value_oldest_joined => sort_title_oldest_joined, + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_oldest_signin => sort_title_oldest_signin + } + end + def sort_title_priority 'Priority' end @@ -79,6 +93,10 @@ module SortingHelper 'Largest repository' end + def sort_title_largest_group + 'Largest group' + end + def sort_title_recently_signin 'Recent sign in' end @@ -95,6 +113,50 @@ module SortingHelper 'Most popular' end + def sort_title_last_joined + 'Last joined' + end + + def sort_title_oldest_joined + 'Oldest joined' + end + + def sort_title_access_level_asc + 'Access level, ascending' + end + + def sort_title_access_level_desc + 'Access level, descending' + end + + def sort_title_name_asc + 'Name, ascending' + end + + def sort_title_name_desc + 'Name, descending' + end + + def sort_value_last_joined + 'last_joined' + end + + def sort_value_oldest_joined + 'oldest_joined' + end + + def sort_value_access_level_asc + 'access_level_asc' + end + + def sort_value_access_level_desc + 'access_level_desc' + end + + def sort_value_name_desc + 'name_desc' + end + def sort_value_priority 'priority' end @@ -136,7 +198,11 @@ module SortingHelper end def sort_value_largest_repo - 'repository_size_desc' + 'storage_size_desc' + end + + def sort_value_largest_group + 'storage_size_desc' end def sort_value_recently_signin diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..e19c67a37ca0c46c38b076b2a05d1d648cdca618 --- /dev/null +++ b/app/helpers/storage_helper.rb @@ -0,0 +1,7 @@ +module StorageHelper + def storage_counter(size_in_bytes) + precision = size_in_bytes < 1.megabyte ? 0 : 1 + + number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false) + end +end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 563ddd2a5118194c866e0e863a928fd1546d13f6..547f62589097198fd112d4ca26033ea15e2903e0 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -106,9 +106,9 @@ module TabHelper def branches_tab_class if current_controller?(:protected_branches) || - current_controller?(:branches) || - current_page?(namespace_project_repository_path(@project.namespace, - @project)) + current_controller?(:branches) || + current_page?(namespace_project_repository_path(@project.namespace, + @project)) 'active' end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 09c6978679137bc1e69b71d448d8f8a4c5340819..74de25acf9d209f02aea5576e8e82a72fd68a6c4 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -35,7 +35,7 @@ module TodosHelper else path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] - path.unshift(:builds) if todo.build_failed? + path.unshift(:pipelines) if todo.build_failed? polymorphic_path(path, anchor: anchor) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bf463a3b6bbd6d26e54a756f46ba7f58091dbd65..8fab77cda0aa8293f846d320a7a330156bd6ff03 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :koding_enabled + validates :plantuml_url, + presence: true, + if: :plantuml_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -184,6 +188,8 @@ class ApplicationSetting < ActiveRecord::Base akismet_enabled: false, koding_enabled: false, koding_url: nil, + plantuml_enabled: false, + plantuml_url: nil, repository_checks_enabled: true, disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5c84833b8068e2a803e726905d8404672920c93b..f6f93debaf32da56dc3fc586357ea46d679bbb61 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,8 +9,16 @@ module Ci has_many :deployments, as: :deployable + # The "environment" field for builds is a String, and is the unexpanded name + def persisted_environment + @persisted_environment ||= Environment.find_by( + name: expanded_environment_name, + project_id: gl_project_id + ) + end + serialize :options - serialize :yaml_variables + serialize :yaml_variables, Gitlab::Serialize::Ci::Variables validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref @@ -35,6 +43,8 @@ module Ci before_destroy { project } after_create :execute_hooks + after_save :update_project_statistics, if: :artifacts_size_changed? + after_destroy :update_project_statistics class << self def first_pending @@ -106,6 +116,12 @@ module Ci end end + def detailed_status(current_user) + Gitlab::Ci::Status::Build::Factory + .new(self, current_user) + .fabricate! + end + def manual? self.when == 'manual' end @@ -129,8 +145,13 @@ module Ci end end + def cancelable? + active? + end + def retryable? - project.builds_enabled? && commands.present? && complete? + project.builds_enabled? && commands.present? && + (success? || failed? || canceled?) end def retried? @@ -138,11 +159,11 @@ module Ci end def expanded_environment_name - ExpandVariables.expand(environment, variables) if environment + ExpandVariables.expand(environment, simple_variables) if environment end def has_environment? - self.environment.present? + environment.present? end def starts_environment? @@ -154,7 +175,7 @@ module Ci end def environment_action - self.options.fetch(:environment, {}).fetch(:action, 'start') + self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end def outdated_deployment? @@ -190,12 +211,25 @@ module Ci project.build_timeout end - def variables + # A slugified version of the build ref, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + def ref_slug + slugified = ref.to_s.downcase + slugified.gsub(/[^a-z0-9]/, '-')[0..62] + end + + # Variables whose value does not depend on other variables + def simple_variables variables = predefined_variables variables += project.predefined_variables variables += pipeline.predefined_variables variables += runner.predefined_variables if runner variables += project.container_registry_variables + variables += project.deployment_variables if has_environment? variables += yaml_variables variables += user_variables variables += project.secret_variables @@ -203,6 +237,13 @@ module Ci variables end + # All variables, including those dependent on other variables + def variables + variables = simple_variables + variables += persisted_environment.predefined_variables if persisted_environment.present? + variables + end + def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) .where(source_branch: ref, source_project_id: pipeline.gl_project_id) @@ -472,6 +513,10 @@ module Ci end end + def has_expiring_artifacts? + artifacts_expire_at.present? + end + def keep_artifacts! self.update(artifacts_expire_at: nil) end @@ -524,6 +569,7 @@ module Ci { key: 'CI_BUILD_REF', value: sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, + { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true }, { key: 'CI_BUILD_STAGE', value: stage, public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, @@ -550,5 +596,9 @@ module Ci Ci::MaskSecret.mask!(trace, token) trace end + + def update_project_statistics + ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size]) + end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fda8228a1e988dc778fbc445353401281742b8c3..2a97e8bae4a8ea9785fd5402c2945ae9fee3d711 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -88,8 +88,21 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest, ->(ref = nil) do + max_id = unscope(:select) + .select("max(#{quoted_table_name}.id)") + .group(:ref, :sha) + + relation = ref ? where(ref: ref) : self + relation.where(id: max_id) + end + + def self.latest_status(ref = nil) + latest(ref).status + end + def self.latest_successful_for(ref) - where(ref: ref).order(id: :desc).success.first + success.latest(ref).order(id: :desc).first end def self.truncate_sha(sha) @@ -100,6 +113,11 @@ module Ci where.not(duration: nil).sum(:duration) end + def stage(name) + stage = Ci::Stage.new(self, name: name) + stage unless stage.statuses_count.zero? + end + def stages_count statuses.select(:stage).distinct.count end @@ -124,7 +142,7 @@ module Ci end def artifacts - builds.latest.with_artifacts_not_expired + builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) end def project_id @@ -173,7 +191,11 @@ module Ci end def manual_actions - builds.latest.manual_actions + builds.latest.manual_actions.includes(project: [:namespace]) + end + + def stuck? + builds.pending.any?(&:stuck?) end def retryable? @@ -265,6 +287,10 @@ module Ci end end + def has_yaml_errors? + yaml_errors.present? + end + def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end @@ -336,8 +362,10 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end - def detailed_status - Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Pipeline::Factory + .new(self, current_user) + .fabricate! end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d2a37c0a827a49f3a7ff6a5c08d52496c9a452c1..d035eda6df56084f45a97a673e0b1e95f0464314 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -18,12 +18,18 @@ module Ci name end + def statuses_count + @statuses_count ||= statuses.count + end + def status @status ||= statuses.latest.status end - def detailed_status - Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! end def statuses diff --git a/app/models/commit.rb b/app/models/commit.rb index 1831cc7e175586ac3270b6154776773db4ed520a..3365f4ffdbfeb58d69255de1f77e725303f64977 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -91,8 +91,8 @@ class Commit @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) end - def to_reference(from_project = nil) - commit_reference(from_project, id) + def to_reference(from_project = nil, full: false) + commit_reference(from_project, id, full: full) end def reference_link_text(from_project = nil) @@ -228,13 +228,9 @@ class Commit def status(ref = nil) @statuses ||= {} - if @statuses.key?(ref) - @statuses[ref] - elsif ref - @statuses[ref] = pipelines.where(ref: ref).status - else - @statuses[ref] = pipelines.status - end + return @statuses[ref] if @statuses.key?(ref) + + @statuses[ref] = pipelines.latest_status(ref) end def revert_branch_name @@ -270,7 +266,7 @@ class Commit @merged_merge_request_hash ||= Hash.new do |hash, user| hash[user] = merged_merge_request_no_cache(user) end - + @merged_merge_request_hash[current_user] end @@ -322,10 +318,18 @@ class Commit Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end + def persisted? + true + end + + def touch + # no-op but needs to be defined since #persisted? is defined + end + private - def commit_reference(from_project, referable_commit_id) - reference = project.to_reference(from_project) + def commit_reference(from_project, referable_commit_id, full: false) + reference = project.to_reference(from_project, full: full) if reference.present? "#{reference}#{self.class.reference_prefix}#{referable_commit_id}" diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index d9af7f6c13990f2ae97401a3eb076be3c94eef61..84e2e8a5dd5b8e6e2fce0489be899770b05bb321 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -89,8 +89,8 @@ class CommitRange alias_method :id, :to_s - def to_reference(from_project = nil) - project_reference = project.to_reference(from_project) + def to_reference(from_project = nil, full: false) + project_reference = project.to_reference(from_project, full: full) if project_reference.present? project_reference + self.class.reference_prefix + self.id diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index cf90475f4d4502cd173cdec37156a1cf1aff7643..9547c57b2ae3374dd2b5f040cf07a074af09a193 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -131,4 +131,16 @@ class CommitStatus < ActiveRecord::Base def has_trace? false end + + def detailed_status(current_user) + Gitlab::Ci::Status::Factory + .new(self, current_user) + .fabricate! + end + + def sortable_name + name.split(/(\d+)/).map do |v| + v =~ /\d+/ ? v.to_i : v + end + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0ea7b1b1098e3786c5d850613b2013a9dd4e3ab2..3517969eabc65b45ae612944c8e525df1ffffbc9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -13,6 +13,7 @@ module Issuable include StripAttribute include Awardable include Taskable + include TimeTrackable included do cache_markdown_field :title, pipeline: :single_line @@ -92,8 +93,9 @@ module Issuable after_save :record_metrics def update_assignee_cache_counts - # make sure we flush the cache for both the old *and* new assignee - User.find(assignee_id_was).update_cache_counts if assignee_id_was + # make sure we flush the cache for both the old *and* new assignees(if they exist) + previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was + previous_assignee.update_cache_counts if previous_assignee assignee.update_cache_counts if assignee end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 875e983448714ca1c0ca9993e6c6c5fe8eb0bb22..e9450dd0c26cf331a53f9ca8358a6b6ff23df8c1 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,10 +1,18 @@ module Milestoneish def closed_items_count(user) - issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size + memoize_per_user(user, :closed_items_count) do + (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size + end end def total_items_count(user) - issues_visible_to_user(user).size + merge_requests.size + memoize_per_user(user, :total_items_count) do + total_issues_count(user) + merge_requests.size + end + end + + def total_issues_count(user) + count_issues_by_state(user).values.sum end def complete?(user) @@ -30,7 +38,10 @@ module Milestoneish end def issues_visible_to_user(user) - issues.visible_to_user(user) + memoize_per_user(user, :issues_visible_to_user) do + IssuesFinder.new(user, issues_finder_params) + .execute.where(milestone_id: milestoneish_ids) + end end def upcoming? @@ -50,4 +61,24 @@ module Milestoneish def expired? due_date && due_date.past? end + + private + + def count_issues_by_state(user) + memoize_per_user(user, :count_issues_by_state) do + issues_visible_to_user(user).reorder(nil).group(:state).count + end + end + + def memoize_per_user(user, method_name) + @memoized ||= {} + @memoized[method_name] ||= {} + @memoized[method_name][user.try!(:id)] ||= yield + end + + # override in a class that includes this module to get a faster query + # from IssuesFinder + def issues_finder_params + {} + end end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 6d88951c7135bd3c9322dba937272ed9626c7388..60734bc666040033eddcf2c980913ff322b94f57 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.update_attribute(field, access_level) + project_feature.send(:write_attribute, field, access_level) end end diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb new file mode 100644 index 0000000000000000000000000000000000000000..2589215ad19dce10783c50a723548d3616534a99 --- /dev/null +++ b/app/models/concerns/reactive_caching.rb @@ -0,0 +1,118 @@ +# The ReactiveCaching concern is used to fetch some data in the background and +# store it in the Rails cache, keeping it up-to-date for as long as it is being +# requested. If the data hasn't been requested for +reactive_cache_lifetime+, +# it stop being refreshed, and then be removed. +# +# Example of use: +# +# class Foo < ActiveRecord::Base +# include ReactiveCaching +# +# self.reactive_cache_key = ->(thing) { ["foo", thing.id] } +# +# after_save :clear_reactive_cache! +# +# def calculate_reactive_cache +# # Expensive operation here. The return value of this method is cached +# end +# +# def result +# with_reactive_cache do |data| +# # ... +# end +# end +# end +# +# In this example, the first time `#result` is called, it will return `nil`. +# However, it will enqueue a background worker to call `#calculate_reactive_cache` +# and set an initial cache lifetime of ten minutes. +# +# Each time the background job completes, it stores the return value of +# `#calculate_reactive_cache`. It is also re-enqueued to run again after +# `reactive_cache_refresh_interval`, so keeping the stored value up to date. +# Calculations are never run concurrently. +# +# Calling `#result` while a value is in the cache will call the block given to +# `#with_reactive_cache`, yielding the cached value. It will also extend the +# lifetime by `reactive_cache_lifetime`. +# +# Once the lifetime has expired, no more background jobs will be enqueued and +# calling `#result` will again return `nil` - starting the process all over +# again +module ReactiveCaching + extend ActiveSupport::Concern + + included do + class_attribute :reactive_cache_lease_timeout + + class_attribute :reactive_cache_key + class_attribute :reactive_cache_lifetime + class_attribute :reactive_cache_refresh_interval + + # defaults + self.reactive_cache_lease_timeout = 2.minutes + + self.reactive_cache_refresh_interval = 1.minute + self.reactive_cache_lifetime = 10.minutes + + def calculate_reactive_cache(*args) + raise NotImplementedError + end + + def with_reactive_cache(*args, &blk) + within_reactive_cache_lifetime(*args) do + data = Rails.cache.read(full_reactive_cache_key(*args)) + yield data if data.present? + end + ensure + Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) + ReactiveCachingWorker.perform_async(self.class, id, *args) + end + + def clear_reactive_cache!(*args) + Rails.cache.delete(full_reactive_cache_key(*args)) + end + + def exclusively_update_reactive_cache!(*args) + locking_reactive_cache(*args) do + within_reactive_cache_lifetime(*args) do + enqueuing_update(*args) do + value = calculate_reactive_cache(*args) + Rails.cache.write(full_reactive_cache_key(*args), value) + end + end + end + end + + private + + def full_reactive_cache_key(*qualifiers) + prefix = self.class.reactive_cache_key + prefix = prefix.call(self) if prefix.respond_to?(:call) + + ([prefix].flatten + qualifiers).join(':') + end + + def alive_reactive_cache_key(*qualifiers) + full_reactive_cache_key(*(qualifiers + ['alive'])) + end + + def locking_reactive_cache(*args) + lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout) + uuid = lease.try_obtain + yield if uuid + ensure + Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid) + end + + def within_reactive_cache_lifetime(*args) + yield if Rails.cache.read(alive_reactive_cache_key(*args)) + end + + def enqueuing_update(*args) + yield + ensure + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) + end + end +end diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..e1f868a299bbb820b44092042f6500b8d23dffdb --- /dev/null +++ b/app/models/concerns/reactive_service.rb @@ -0,0 +1,10 @@ +module ReactiveService + extend ActiveSupport::Concern + + included do + include ReactiveCaching + + # Default cache key: class name + project_id + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + end +end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 8ba009fe04ffb32f9257d8c87aa903ce8551b613..da803c7f481df0a9b6d15961a1bb6387e29b4216 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -17,7 +17,7 @@ module Referable # Issue.last.to_reference(other_project) # => "cross-project#1" # # Returns a String - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full:) '' end diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb new file mode 100644 index 0000000000000000000000000000000000000000..040e3a2884e08b4a027eda91de80ea267fb9a7ce --- /dev/null +++ b/app/models/concerns/time_trackable.rb @@ -0,0 +1,72 @@ +# == TimeTrackable concern +# +# Contains functionality related to objects that support time tracking. +# +# Used by Issue and MergeRequest. +# + +module TimeTrackable + extend ActiveSupport::Concern + + included do + attr_reader :time_spent, :time_spent_user + + alias_method :time_spent?, :time_spent + + default_value_for :time_estimate, value: 0, allows_nil: false + + validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false + validate :check_negative_time_spent + + has_many :timelogs, as: :trackable, dependent: :destroy + end + + def spend_time(options) + @time_spent = options[:duration] + @time_spent_user = options[:user] + @original_total_time_spent = nil + + return if @time_spent == 0 + + if @time_spent == :reset + reset_spent_time + else + add_or_subtract_spent_time + end + end + alias_method :spend_time=, :spend_time + + def total_time_spent + timelogs.sum(:time_spent) + end + + def human_total_time_spent + Gitlab::TimeTrackingFormatter.output(total_time_spent) + end + + def human_time_estimate + Gitlab::TimeTrackingFormatter.output(time_estimate) + end + + private + + def reset_spent_time + timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) + end + + def add_or_subtract_spent_time + timelogs.new(time_spent: time_spent, user: @time_spent_user) + end + + def check_negative_time_spent + return if time_spent.nil? || time_spent == :reset + + # we need to cache the total time spent so multiple calls to #valid? + # doesn't give a false error + @original_total_time_spent ||= total_time_spent + + if time_spent < 0 && (time_spent.abs > @original_total_time_spent) + errors.add(:time_spent, 'Time to subtract exceeds the total time spent') + end + end +end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 04d30f462101b4428c1006db830265a5661bad2a..1ca7f91dc037fdd582e32faffed91c92b89a7418 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -39,6 +39,10 @@ module TokenAuthenticatable current_token.blank? ? write_new_token(token_field) : current_token end + define_method("set_#{token_field}") do |token| + write_attribute(token_field, token) if token + end + define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? read_attribute(token_field) diff --git a/app/models/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c35cea8d58310bb93446c2b4a0f734bbb8bc713 --- /dev/null +++ b/app/models/concerns/valid_attribute.rb @@ -0,0 +1,10 @@ +module ValidAttribute + extend ActiveSupport::Concern + + # Checks whether an attribute has failed validation or not + # + # +attribute+ The symbolised name of the attribute i.e :name + def valid_attribute?(attribute) + self.errors.empty? || self.errors.messages[attribute].nil? + end +end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index ba4ee6fcf9d8d3d8767abf31583e45b0c4e0029d..d2e626c22e833ad2a39a865802bcd4af7f4130b3 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,62 +1,38 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, current_user, from:) + def initialize(project, options) @project = project - @current_user = current_user - @from = from - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil) + @options = options end def summary - @summary ||= Summary.new(@project, @current_user, from: @from) + @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, + from: @options[:from], + current_user: @options[:current_user]).data end - def permissions(user:) - Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) + def stats + @stats ||= stats_per_stage end - def issue - @fetcher.calculate_metric(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + def no_stats? + stats.all? { |hash| hash[:value].nil? } end - def plan - @fetcher.calculate_metric(:plan, - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]], - Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) - end - - def code - @fetcher.calculate_metric(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) - end - - def test - @fetcher.calculate_metric(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + def permissions(user:) + Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end - def review - @fetcher.calculate_metric(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) + def [](stage_name) + Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) end - def staging - @fetcher.calculate_metric(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) - end + private - def production - @fetcher.calculate_metric(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def stats_per_stage + STAGES.map do |stage_name| + self[stage_name].as_json + end end end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index 82f53d17ddd06d39e624cc0a2bf89a72d5cd09ca..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,43 +0,0 @@ -class CycleAnalytics - class Summary - def initialize(project, current_user, from:) - @project = project - @current_user = current_user - @from = from - end - - def new_issues - IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count - end - - def commits - ref = @project.default_branch.presence - count_commits_for(ref) - end - - def deploys - @project.deployments.where("created_at > ?", @from).count - end - - private - - # Don't use the `Gitlab::Git::Repository#log` method, because it enforces - # a limit. Since we need a commit count, we _can't_ enforce a limit, so - # the easiest way forward is to replicate the relevant portions of the - # `log` function here. - def count_commits_for(ref) - return unless ref - - repository = @project.repository.raw_repository - sha = @project.repository.commit(ref).sha - - cmd = %W(git --git-dir=#{repository.path} log) - cmd << '--format=%H' - cmd << "--after=#{@from.iso8601}" - cmd << sha - - raw_output = IO.popen(cmd) { |io| io.read } - raw_output.lines.count - end - end -end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 2c525d4cd7a78062e759c2c36a4b2a058bd8a58d..053f2a11aa02699556aba609562bfa12ce0207f0 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -20,4 +20,18 @@ class DeployKey < Key def destroyed_when_orphaned? self.private? end + + def has_access_to?(project) + projects.include?(project) + end + + def can_push_to?(project) + can_push? && has_access_to?(project) + end + + private + + # we don't want to notify the user for deploy keys + def notify_user + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 96700143ddd9893eb4d930eb4f5e4a8e59670e65..652abf18a8a145da9be6d5433283986e452dd47f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -1,9 +1,15 @@ class Environment < ActiveRecord::Base + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + belongs_to :project, required: true, validate: true has_many :deployments before_validation :nullify_external_url + before_validation :generate_slug, if: ->(env) { env.slug.blank? } + before_save :set_environment_type validates :name, @@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :slug, + presence: true, + uniqueness: { scope: :project_id }, + length: { maximum: 24 }, + format: { with: Gitlab::Regex.environment_slug_regex, + message: Gitlab::Regex.environment_slug_regex_message } + validates :external_url, uniqueness: { scope: :project_id }, length: { maximum: 255 }, @@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base state :stopped end + def predefined_variables + [ + { key: 'CI_ENVIRONMENT_NAME', value: name, public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true }, + ] + end + def recently_updated_on_branch?(ref) ref.to_s == last_deployment.try(:ref) end @@ -67,7 +87,7 @@ class Environment < ActiveRecord::Base end def update_merge_request_metrics? - self.name == "production" + (environment_type || name) == "production" end def first_deployment_for(commit) @@ -107,4 +127,49 @@ class Environment < ActiveRecord::Base action.expanded_environment_name == environment end end + + def has_terminals? + project.deployment_service.present? && available? && last_deployment.present? + end + + def terminals + project.deployment_service.terminals(self) if has_terminals? + end + + # An environment name is not necessarily suitable for use in URLs, DNS + # or other third-party contexts, so provide a slugified version. A slug has + # the following properties: + # * contains only lowercase letters (a-z), numbers (0-9), and '-' + # * begins with a letter + # * has a maximum length of 24 bytes (OpenShift limitation) + # * cannot end with `-` + def generate_slug + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + self.slug = slugified + end + + private + + # Slugifying a name may remove the uniqueness guarantee afforded by it being + # based on name (which must be unique). To compensate, we add a random + # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness, + # but the chance of collisions is vanishingly small + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 91b508eb3251d9893c5ddae90bae4f318446dd7d..26712c19b5a3b35c9935a3032ce2e5d601d246a7 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -38,7 +38,7 @@ class ExternalIssue @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full: nil) id end diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 9803bae0beec8dd8e32ad9f7999dec0ef761ab1f..36cf7ad6a280584645d975ada0130c2059c1c11c 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,4 +1,4 @@ class ForkedProjectLink < ActiveRecord::Base - belongs_to :forked_to_project, class_name: Project - belongs_to :forked_from_project, class_name: Project + belongs_to :forked_to_project, class_name: 'Project' + belongs_to :forked_from_project, class_name: 'Project' end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index b01607dcda9aa8e96e98f7d2ac17c8e9f880abf1..b991d78e27fc22bbd503300c83c52e39b4ed02f1 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,6 +1,8 @@ class GlobalMilestone include Milestoneish + EPOCH = DateTime.parse('1970-01-01') + attr_accessor :title, :milestones alias_attribute :name, :title @@ -8,13 +10,22 @@ class GlobalMilestone @first_milestone end - def self.build_collection(milestones) - milestones = milestones.group_by(&:title) + def self.build_collection(projects, params) + child_milestones = MilestonesFinder.new.execute(projects, params) - milestones.map do |title, milestones| - milestones_relation = Milestone.where(id: milestones.map(&:id)) + milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped| + milestones_relation = Milestone.where(id: grouped.map(&:id)) new(title, milestones_relation) end + + milestones.sort_by { |milestone| milestone.due_date || EPOCH } + end + + def self.build(projects, title) + child_milestones = Milestone.of_projects(projects).where(title: title) + return if child_milestones.blank? + + new(title, child_milestones) end def initialize(title, milestones) @@ -24,12 +35,16 @@ class GlobalMilestone @first_milestone = milestones.find {|m| m.description.present? } || milestones.first end + def milestoneish_ids + milestones.select(:id) + end + def safe_title @title.to_slug.normalize.to_s end def projects - @projects ||= Project.for_milestones(milestones.select(:id)) + @projects ||= Project.for_milestones(milestoneish_ids) end def state @@ -49,11 +64,11 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) + @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels) + @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels) end def participants diff --git a/app/models/group.rb b/app/models/group.rb index 4248e1162d85edcdd32fbae8de65db7547957d7d..99675ddb3661ff5c9f6ca515f689d0b5d3cd263d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -48,7 +48,13 @@ class Group < Namespace end def sort(method) - order_by(method) + if method == 'storage_size_desc' + # storage_size is a virtual column so we need to + # pass a string to avoid AR adding the table name + reorder('storage_size DESC, namespaces.id DESC') + else + order_by(method) + end end def reference_prefix @@ -74,7 +80,7 @@ class Group < Namespace end end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full: nil) "#{self.class.reference_prefix}#{name}" end @@ -83,7 +89,7 @@ class Group < Namespace end def human_name - name + full_name end def visibility_level_field @@ -155,15 +161,17 @@ class Group < Namespace end def has_owner?(user) - owners.include?(user) + members_with_parents.owners.where(user_id: user).any? end def has_master?(user) - members.masters.where(user_id: user).any? + members_with_parents.masters.where(user_id: user).any? end + # Check if user is a last owner of the group. + # Parent owners are ignored for nested groups. def last_owner?(user) - has_owner?(user) && owners.size == 1 + owners.include?(user) && owners.size == 1 end def avatar_type @@ -189,6 +197,14 @@ class Group < Namespace end def refresh_members_authorized_projects - UserProjectAccessChangedService.new(users.pluck(:id)).execute + UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute + end + + def members_with_parents + GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id)) + end + + def users_with_parents + User.where(id: members_with_parents.select(:user_id)) end end diff --git a/app/models/group_label.rb b/app/models/group_label.rb index 68841ace2e6b62698c6f5d73e66eab45c360cb27..92c83b54861debec8f627dce1a24649757aa5a3a 100644 --- a/app/models/group_label.rb +++ b/app/models/group_label.rb @@ -8,8 +8,4 @@ class GroupLabel < Label def subject_foreign_key 'group_id' end - - def to_reference(source_project = nil, target_project = nil, format: :id) - super(source_project, target_project, format: format) - end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb new file mode 100644 index 0000000000000000000000000000000000000000..7b6db2634b7107a4cebb2ae55d9961f4fe909a2d --- /dev/null +++ b/app/models/group_milestone.rb @@ -0,0 +1,19 @@ +class GroupMilestone < GlobalMilestone + attr_accessor :group + + def self.build_collection(group, projects, params) + super(projects, params).each do |milestone| + milestone.group = group + end + end + + def self.build(group, projects, title) + super(projects, title).tap do |milestone| + milestone.group = group if milestone + end + end + + def issues_finder_params + { group_id: group.id } + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 7fe92051037649e93dd34afbc32bb90e650ddcd1..65638d9a29957e9ac0433cff9e4604701da901e3 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } + scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true @@ -60,61 +62,6 @@ class Issue < ActiveRecord::Base attributes end - class << self - private - - # Returns the project that the current scope belongs to if any, nil otherwise. - # - # Examples: - # - my_project.issues.without_due_date.owner_project => my_project - # - Issue.all.owner_project => nil - def owner_project - # No owner if we're not being called from an association - return unless all.respond_to?(:proxy_association) - - owner = all.proxy_association.owner - - # Check if the association is or belongs to a project - if owner.is_a?(Project) - owner - else - begin - owner.association(:project).target - rescue ActiveRecord::AssociationNotFoundError - nil - end - end - end - end - - def self.visible_to_user(user) - return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return all if user.admin? - - # Check if we are scoped to a specific project's issues - if owner_project - if owner_project.team.member?(user, Gitlab::Access::REPORTER) - # If the project is authorized for the user, they can see all issues in the project - return all - else - # else only non confidential and authored/assigned to them - return where('issues.confidential IS NULL OR issues.confidential IS FALSE - OR issues.author_id = :user_id OR issues.assignee_id = :user_id', - user_id: user.id) - end - end - - where(' - issues.confidential IS NULL - OR issues.confidential IS FALSE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR issues.assignee_id = :user_id - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) - end - def self.reference_prefix '#' end @@ -150,10 +97,10 @@ class Issue < ActiveRecord::Base end end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" end def referenced_merge_requests(current_user = nil) diff --git a/app/models/key.rb b/app/models/key.rb index a5d25409730cb0f699eb3847d8c4626c6b9efa80..8be29c697f1bc62ea8cf4f64bf43143d6c98ff90 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -49,6 +49,10 @@ class Key < ActiveRecord::Base "key-#{id}" end + def update_last_used_at + UseKeyWorker.perform_async(self.id) + end + def add_to_shell GitlabShellWorker.perform_async( :add_key, @@ -57,10 +61,6 @@ class Key < ActiveRecord::Base ) end - def notify_user - run_after_commit { NotificationService.new.new_key(self) } - end - def post_create_hook SystemHooksService.new.execute_hooks_for(self, :create) end @@ -86,4 +86,8 @@ class Key < ActiveRecord::Base self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint end + + def notify_user + run_after_commit { NotificationService.new.new_key(self) } + end end diff --git a/app/models/label.rb b/app/models/label.rb index d38c37344c9cacb81729c6e1a9a08227bb8714e3..5b6b9a7a736798ca84c9fe27a02dbf8fa95adcb8 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -26,6 +26,7 @@ class Label < ActiveRecord::Base # Don't allow ',' for label titles validates :title, presence: true, format: { with: /\A[^,]+\z/ } validates :title, uniqueness: { scope: [:group_id, :project_id] } + validates :title, length: { maximum: 255 } default_scope { order(title: :asc) } @@ -146,17 +147,17 @@ class Label < ActiveRecord::Base # # Label.first.to_reference # => "~1" # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1" - # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference(project, target_project: same_namespace_project) # => "gitlab-ce~1" + # Label.first.to_reference(project, target_project: another_namespace_project) # => "gitlab-org/gitlab-ce~1" # # Returns a String # - def to_reference(source_project = nil, target_project = nil, format: :id) + def to_reference(from_project = nil, target_project: nil, format: :id, full: false) format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if source_project - "#{source_project.to_reference(target_project)}#{reference}" + if from_project + "#{from_project.to_reference(target_project, full: full)}#{reference}" else reference end diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 0fd5f089db9076d03d7c17fcea5518964d688809..007eed5600a0b47e146f477948731b5f30f480ba 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base validates :lfs_object_id, presence: true validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true + + after_create :update_project_statistics + after_destroy :update_project_statistics + + private + + def update_project_statistics + ProjectCacheWorker.perform_async(project_id, [], [:lfs_objects_size]) + end end diff --git a/app/models/member.rb b/app/models/member.rb index 3b65587c66b0add69feb96548d32fc120ad103aa..c585e0b450e97d8e22b29631b828eddaa19cfd39 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -57,6 +57,11 @@ class Member < ActiveRecord::Base scope :owners, -> { active.where(access_level: OWNER) } scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) } + scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } + scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } + scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) } + scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) } + before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite?, unless: :importing? @@ -72,6 +77,34 @@ class Member < ActiveRecord::Base default_value_for :notification_level, NotificationSetting.levels[:global] class << self + def search(query) + joins(:user).merge(User.search(query)) + end + + def sort(method) + case method.to_s + when 'access_level_asc' then reorder(access_level: :asc) + when 'access_level_desc' then reorder(access_level: :desc) + when 'recent_sign_in' then order_recent_sign_in + when 'oldest_sign_in' then order_oldest_sign_in + when 'last_joined' then order_created_desc + when 'oldest_joined' then order_created_asc + else + order_by(method) + end + end + + def left_join_users + users = User.arel_table + members = Member.arel_table + + member_users = members.join(users, Arel::Nodes::OuterJoin). + on(members[:user_id].eq(users[:id])). + join_sources + + joins(member_users) + end + def access_for_user_ids(user_ids) where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h end @@ -89,8 +122,8 @@ class Member < ActiveRecord::Base member = if user.is_a?(User) source.members.find_by(user_id: user.id) || - source.requesters.find_by(user_id: user.id) || - source.members.build(user_id: user.id) + source.requesters.find_by(user_id: user.id) || + source.members.build(user_id: user.id) else source.members.build(invite_email: user) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ea3cf1cdaac46a47dfa24bb01a7e3486414e104b..10251302db8cc0e69adc4883f25dba2a5c3ac05e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -97,7 +97,7 @@ class MergeRequest < ActiveRecord::Base validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true - validates :merge_user, presence: true, if: :merge_when_build_succeeds? + validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? @@ -175,10 +175,10 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" end def first_commit @@ -198,7 +198,9 @@ class MergeRequest < ActiveRecord::Base end def diff_size - diffs(diff_options).size + opts = diff_options || {} + + raw_diffs(opts).size end def diff_base_commit @@ -219,7 +221,7 @@ class MergeRequest < ActiveRecord::Base # true base commit, so we can't simply have `#diff_base_commit` fall back on # this method. def likely_diff_base_commit - first_commit.parent || first_commit + first_commit.try(:parent) || first_commit end def diff_start_commit @@ -452,7 +454,7 @@ class MergeRequest < ActiveRecord::Base should_remove_source_branch? || force_remove_source_branch? end - def mr_and_commit_notes + def related_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) @@ -468,7 +470,7 @@ class MergeRequest < ActiveRecord::Base end def discussions - @discussions ||= self.mr_and_commit_notes. + @discussions ||= self.related_notes. inc_relations_for_view. fresh. discussions @@ -568,6 +570,15 @@ class MergeRequest < ActiveRecord::Base end end + def issues_mentioned_but_not_closing(current_user = self.author) + return [] unless target_branch == project.default_branch + + ext = Gitlab::ReferenceExtractor.new(project, current_user) + ext.analyze(description) + + ext.issues - closes_issues + end + def target_project_path if target_project target_project.path_with_namespace @@ -612,13 +623,24 @@ class MergeRequest < ActiveRecord::Base self.target_project.repository.branch_names.include?(self.target_branch) end - def merge_commit_message - message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" - message << "#{title}\n\n" - message << "#{description}\n\n" if description.present? + def merge_commit_message(include_description: false) + closes_issues_references = closes_issues.map do |issue| + issue.to_reference(target_project) + end + + message = [ + "Merge branch '#{source_branch}' into '#{target_branch}'", + title + ] + + if !include_description && closes_issues_references.present? + message << "Closes #{closes_issues_references.to_sentence}" + end + + message << "#{description}" if include_description && description.present? message << "See merge request #{to_reference}" - message + message.join("\n\n") end def reset_merge_when_build_succeeds @@ -876,10 +898,22 @@ class MergeRequest < ActiveRecord::Base end def has_commits? - commits_count > 0 + merge_request_diff && commits_count > 0 end def has_no_commits? !has_commits? end + + def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil) + return false unless can_be_merged_by?(current_user) + + return true if autocomplete_precheck + + return false unless mergeable?(skip_ci_check: true) + return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?) + return false if last_diff_sha != diff_head_sha + + true + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b8f36a2c958fd6be30ac0db06b09ea34b0b0e606..dadb81f9b6e30804cf984d98e63e9acdcc3dd5ec 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,7 +1,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable include Importable - include EncodingHelper + include Gitlab::Git::EncodingHelper # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 @@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base # and save it as array of hashes in st_diffs db field def save_diffs new_attributes = {} - new_diffs = [] if commits.size.zero? new_attributes[:state] = :empty else diff_collection = compare.diffs(Commit.max_diff_options) - - if diff_collection.overflow? - # Set our state to 'overflow' to make the #empty? and #collected? - # methods (generated by StateMachine) return false. - new_attributes[:state] = :overflow - end - - new_attributes[:real_size] = diff_collection.real_size + new_attributes[:real_size] = compare.diffs.real_size if diff_collection.any? new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected end + + new_attributes[:st_diffs] = new_diffs || [] + + # Set our state to 'overflow' to make the #empty? and #collected? + # methods (generated by StateMachine) return false. + # + # This attribution has to come at the end of the method so 'overflow' + # state does not get overridden by 'collected'. + new_attributes[:state] = :overflow if diff_collection.overflow? end - new_attributes[:st_diffs] = new_diffs update_columns_serialized(new_attributes) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 45ca97adad1436d79209512fa1f830a2544341de..7331000a9f2de4bd43ebc6f2dd2cf573eb54c40d 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -118,17 +118,21 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # - def to_reference(from_project = nil, format: :iid) + def to_reference(from_project = nil, format: :iid, full: false) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" end def reference_link_text(from_project = nil) self.title end + def milestoneish_ids + id + end + def can_be_closed? active? && issues.opened.count.zero? end @@ -198,4 +202,8 @@ class Milestone < ActiveRecord::Base errors.add(:start_date, "Can't be greater than due date") end end + + def issues_finder_params + { project_id: project_id } + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 37374044551da69fe9a5e3f8d75ca06c4035c304..d41833de66f2cd0325092fc5f51227baf703a77f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base cache_markdown_field :description, pipeline: :description has_many :projects, dependent: :destroy + has_many :project_statistics belongs_to :owner, class_name: "User" belongs_to :parent, class_name: "Namespace" @@ -17,14 +18,13 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, presence: true, - uniqueness: true, + uniqueness: { scope: :parent_id }, length: { maximum: 255 }, namespace_name: true validates :description, length: { maximum: 255 } validates :path, presence: true, - uniqueness: { case_sensitive: false }, length: { maximum: 255 }, namespace: true @@ -39,6 +39,18 @@ class Namespace < ActiveRecord::Base scope :root, -> { where('type IS NULL') } + scope :with_statistics, -> do + joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id') + .group('namespaces.id') + .select( + 'namespaces.*', + 'COALESCE(SUM(ps.storage_size), 0) AS storage_size', + 'COALESCE(SUM(ps.repository_size), 0) AS repository_size', + 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size', + 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size', + ) + end + class << self def by_path(path) find_by('lower(path) = :value', value: path.downcase) @@ -99,7 +111,7 @@ class Namespace < ActiveRecord::Base def move_dir if any_project_has_container_registry_tags? - raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -112,7 +124,7 @@ class Namespace < ActiveRecord::Base # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('namespace directory cannot be moved') + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') end end @@ -162,6 +174,19 @@ class Namespace < ActiveRecord::Base end end + def full_name + @full_name ||= + if parent + parent.full_name + ' / ' + name + else + name + end + end + + def parents + @parents ||= parent ? parent.parents + [parent] : [] + end + private def repository_storage_paths diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 345041a6ad18b233cb77a5a016ab5cd5ba670a49..b524ca50ee820e041153a570700dd35436b4b308 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -161,8 +161,8 @@ module Network def is_overlap?(range, overlap_space) range.each do |i| if i != range.first && - i != range.last && - @commits[i].spaces.include?(overlap_space) + i != range.last && + @commits[i].spaces.include?(overlap_space) return true end diff --git a/app/models/note.rb b/app/models/note.rb index 08bd08743efb56df08ec9a82d0fa9231fd17f5a6..0c1b05dabf28909b5aaedec606ebd1308b515a1d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -107,23 +107,6 @@ class Note < ActiveRecord::Base Discussion.for_diff_notes(active_notes). map { |d| [d.line_code, d] }.to_h end - - # Searches for notes matching the given query. - # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. - # - # query - The search query as a String. - # as_user - Limit results to those viewable by a specific user - # - # Returns an ActiveRecord::Relation. - def search(query, as_user: nil) - table = arel_table - pattern = "%#{query}%" - - Note.joins('LEFT JOIN issues ON issues.id = noteable_id'). - where(table[:note].matches(pattern)). - merge(Issue.visible_to_user(as_user)) - end end def cross_reference? diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 43fc218de2b5d458ab85add171d173ab8d2c0880..58f6214bea77ed2209b030de23d4aae767983a20 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ] + EXCLUDED_WATCHER_EVENTS = [ + :success_pipeline + ] + store :events, accessors: EMAIL_EVENTS, coder: JSON before_create :set_events diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index c4b095e0c0471c82e03e5cf97cac05d4fdaa1e40..10a34c42fd8e828c5fb8c9c07d40e889b2c95c20 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :token + serialize :scopes, Array + belongs_to :user scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") } diff --git a/app/models/project.rb b/app/models/project.rb index 77d740081c6cb967042b8d8fa8235a009c7d18d0..1630975b0d3fd23189a76435aa816c12d3149753 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,7 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ValidAttribute include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Routable @@ -44,6 +45,7 @@ class Project < ActiveRecord::Base after_create :ensure_dir_exist after_create :create_project_feature, unless: :project_feature after_save :ensure_dir_exist, if: :namespace_id_changed? + after_save :update_project_statistics, if: :namespace_id_changed? # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -64,6 +66,8 @@ class Project < ActiveRecord::Base end end + after_validation :check_pending_delete + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags @@ -79,7 +83,6 @@ class Project < ActiveRecord::Base has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_many :boards, before_add: :validate_board_limit, dependent: :destroy - has_many :chat_services # Project services has_one :campfire_service, dependent: :destroy @@ -95,6 +98,8 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy + has_one :mattermost_service, dependent: :destroy + has_one :slack_slash_commands_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy @@ -106,6 +111,7 @@ class Project < ActiveRecord::Base has_one :bugzilla_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy + has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -116,7 +122,7 @@ class Project < ActiveRecord::Base # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest + has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy @@ -127,7 +133,7 @@ class Project < ActiveRecord::Base has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy - has_many :project_authorizations, dependent: :destroy + has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :project_members @@ -149,6 +155,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy + has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -218,6 +225,7 @@ class Project < ActiveRecord::Base scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } + scope :with_statistics, -> { includes(:statistics) } # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { @@ -330,8 +338,10 @@ class Project < ActiveRecord::Base end def sort(method) - if method == 'repository_size_desc' - reorder(repository_size: :desc, id: :desc) + if method == 'storage_size_desc' + # storage_size is a joined column so we need to + # pass a string to avoid AR adding the table name + reorder('project_statistics.storage_size DESC, projects.id DESC') else order_by(method) end @@ -531,6 +541,10 @@ class Project < ActiveRecord::Base import_type == 'gitlab_project' end + def gitea_import? + import_type == 'gitea' + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' projects_limit = creator.projects_limit @@ -578,8 +592,8 @@ class Project < ActiveRecord::Base end end - def to_reference(from_project = nil) - if cross_namespace_reference?(from_project) + def to_reference(from_project = nil, full: false) + if full || cross_namespace_reference?(from_project) path_with_namespace elsif cross_project_reference?(from_project) path @@ -598,10 +612,6 @@ class Project < ActiveRecord::Base Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end - def web_url_without_protocol - web_url.split('://')[1] - end - def new_issue_address(author) return unless Gitlab::IncomingEmail.supports_issue_creation? && author @@ -742,6 +752,14 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.reorder(nil).find_by(active: true) end + def deployment_services + services.where(category: :deployment) + end + + def deployment_service + @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end @@ -907,7 +925,7 @@ class Project < ActiveRecord::Base Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" # we currently doesn't support renaming repository if it contains tags in container registry - raise Exception.new('Project cannot be renamed, because tags are present in its container registry') + raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') end if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) @@ -934,7 +952,7 @@ class Project < ActiveRecord::Base # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('repository cannot be renamed') + raise StandardError.new('repository cannot be renamed') end Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" @@ -1014,7 +1032,7 @@ class Project < ActiveRecord::Base "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) - repository.expire_avatar_cache + repository.after_change_head reload_default_branch end @@ -1022,14 +1040,6 @@ class Project < ActiveRecord::Base forked? && project == forked_from_project end - def update_repository_size - update_attribute(:repository_size, repository.size) - end - - def update_commit_count - update_attribute(:commit_count, repository.commit_count) - end - def forks_count forks.count end @@ -1220,6 +1230,12 @@ class Project < ActiveRecord::Base end end + def deployment_variables + return [] unless deployment_service + + deployment_service.predefined_variables + end + def append_or_update_attribute(name, value) old_values = public_send(name.to_s) @@ -1302,4 +1318,26 @@ class Project < ActiveRecord::Base def full_path_changed? path_changed? || namespace_id_changed? end + + def update_project_statistics + stats = statistics || build_statistics + stats.update(namespace_id: namespace_id) + end + + def check_pending_delete + return if valid_attribute?(:name) && valid_attribute?(:path) + return unless pending_delete_twin + + %i[route route.path name path].each do |error| + errors.delete(error) + end + + errors.add(:base, "The project is still being deleted. Please try again later.") + end + + def pending_delete_twin + return false unless path + + Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) + end end diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index a00d43773d9f282155637c2ec942291bfe4b73c3..4c7f4f5a4291856dc8002c0068376fad4b07d865 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base validates :project, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true + + def self.insert_authorizations(rows, per_batch = 1000) + rows.each_slice(per_batch) do |slice| + tuples = slice.map do |tuple| + tuple.map { |value| connection.quote(value) } + end + + connection.execute <<-EOF.strip_heredoc + INSERT INTO project_authorizations (user_id, project_id, access_level) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end + end end diff --git a/app/models/project_label.rb b/app/models/project_label.rb index 82f47f0e8fd407b96abb1320019fc8cb951c7ea1..313815e58691cb629f1fd5892a1623a5a26175ed 100644 --- a/app/models/project_label.rb +++ b/app/models/project_label.rb @@ -16,8 +16,8 @@ class ProjectLabel < Label 'project_id' end - def to_reference(target_project = nil, format: :id) - super(project, target_project, format: format) + def to_reference(target_project = nil, format: :id, full: false) + super(project, target_project: target_project, format: format, full: full) end private diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index b5c76e4d4fef5ca93a219b731f3765fd71d568d9..4819bdbef8cae042fa8f2e1a6e35e830f23f5d8d 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,4 +1,6 @@ class BambooService < CiService + include ReactiveService + prop_accessor :bamboo_url, :build_key, :username, :password validates :bamboo_url, presence: true, url: true, if: :activated? @@ -58,31 +60,46 @@ class BambooService < CiService %w(push) end - def build_info(sha) - @response = get_path("rest/api/latest/result?label=#{sha}") + def build_page(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + end - if @response.code != 200 || @response['results']['results']['size'] == '0' + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + get_path("updateAndBuild.action?buildKey=#{build_key}") + end + + def calculate_reactive_cache(sha, ref) + response = get_path("rest/api/latest/result?label=#{sha}") + + { build_page: read_build_page(response), commit_status: read_commit_status(response) } + end + + private + + def read_build_page(response) + if response.code != 200 || response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s else # If actual build link is available, go to build result page. - result_key = @response['results']['results']['result']['planResultKey']['key'] + result_key = response['results']['results']['result']['planResultKey']['key'] URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s end end - def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 - status = if @response.code == 404 || @response['results']['results']['size'] == '0' + status = if response.code == 404 || response['results']['results']['size'] == '0' 'Pending' else - @response['results']['results']['result']['buildState'] + response['results']['results']['result']['buildState'] end if status.include?('Success') @@ -96,14 +113,6 @@ class BambooService < CiService end end - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - get_path("updateAndBuild.action?buildKey=#{build_key}") - end - - private - def build_url(path) URI.join("#{bamboo_url}/", path).to_s end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index fe6d7aabb22e45b5ff552e199289b9e367d46c40..e77942d8f3cff099a5be9f3f93d51e94469655a1 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,6 +1,8 @@ require "addressable/uri" class BuildkiteService < CiService + include ReactiveService + ENDPOINT = "https://buildkite.com" prop_accessor :project_url, :token @@ -33,13 +35,7 @@ class BuildkiteService < CiService end def commit_status(sha, ref) - response = HTTParty.get(commit_status_path(sha), verify: false) - - if response.code == 200 && response['status'] - response['status'] - else - :error - end + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end def commit_status_path(sha) @@ -78,6 +74,19 @@ class BuildkiteService < CiService ] end + def calculate_reactive_cache(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + + status = + if response.code == 200 && response['status'] + response['status'] + else + :error + end + + { commit_status: status } + end + private def webhook_token diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/chat_message/base_message.rb similarity index 96% rename from app/models/project_services/slack_service/base_message.rb rename to app/models/project_services/chat_message/base_message.rb index f1182824687885584ead06828ff30ab53ebd3f28..a03605d01fb5e507addb059755e0df1fa7b430cf 100644 --- a/app/models/project_services/slack_service/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -1,6 +1,6 @@ require 'slack-notifier' -class SlackService +module ChatMessage class BaseMessage def initialize(params) raise NotImplementedError diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/chat_message/build_message.rb similarity index 98% rename from app/models/project_services/slack_service/build_message.rb rename to app/models/project_services/chat_message/build_message.rb index 0fca4267bad8eb3f9246c40cf09e24a823526d0a..53e35cb21bf0692e3e83ac1e7520209ea1d67bb3 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/chat_message/build_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class BuildMessage < BaseMessage attr_reader :sha attr_reader :ref_type diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb similarity index 98% rename from app/models/project_services/slack_service/issue_message.rb rename to app/models/project_services/chat_message/issue_message.rb index cd87a79d0c607383f6c92b0c66cda190f095dcd4..14fd64e53321fe644ca98e2842e14e690c4ac6a8 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class IssueMessage < BaseMessage attr_reader :user_name attr_reader :title diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb similarity index 98% rename from app/models/project_services/slack_service/merge_message.rb rename to app/models/project_services/chat_message/merge_message.rb index b7615c960686177ef046d6b1ea25e46b6be15c8b..ab5e8b2416775b996bdd3b300bee23765af38fe4 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class MergeMessage < BaseMessage attr_reader :user_name attr_reader :project_name diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/chat_message/note_message.rb similarity index 99% rename from app/models/project_services/slack_service/note_message.rb rename to app/models/project_services/chat_message/note_message.rb index 797c5937f097d8084d2bde57da39231448157862..ca1d72070349c6b1a276fd053b114c71d01fdbfb 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class NoteMessage < BaseMessage attr_reader :message attr_reader :user_name diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb similarity index 95% rename from app/models/project_services/slack_service/pipeline_message.rb rename to app/models/project_services/chat_message/pipeline_message.rb index f8d03c0e2fa3a238d7ec8e226292d4adf2de6415..210027565a8cfe09e04596f95236a180bf439786 100644 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class PipelineMessage < BaseMessage attr_reader :ref_type, :ref, :status, :project_name, :project_url, :user_name, :duration, :pipeline_id @@ -13,7 +13,7 @@ class SlackService @project_name = data[:project][:path_with_namespace] @project_url = data[:project][:web_url] - @user_name = data[:user] && data[:user][:name] + @user_name = (data[:user] && data[:user][:name]) || 'API' end def pretext diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/chat_message/push_message.rb similarity index 99% rename from app/models/project_services/slack_service/push_message.rb rename to app/models/project_services/chat_message/push_message.rb index b26f3e9ddce9f392d6cc06e9446fb03a49fa1de3..2d73b71ec376b11981912d7123a04a41e0dc684d 100644 --- a/app/models/project_services/slack_service/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class PushMessage < BaseMessage attr_reader :after attr_reader :before diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb similarity index 98% rename from app/models/project_services/slack_service/wiki_page_message.rb rename to app/models/project_services/chat_message/wiki_page_message.rb index 160ca3ac11523fa32fe5c5a36f640e1050401c11..134083e4504d639a67df5c54873118d8f47d9049 100644 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class WikiPageMessage < BaseMessage attr_reader :user_name attr_reader :title diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7ef44c3054523edc10666a5f59e1da85387ab3a --- /dev/null +++ b/app/models/project_services/chat_notification_service.rb @@ -0,0 +1,149 @@ +# Base class for Chat notifications services +# This class is not meant to be used directly, but only to inherit from. +class ChatNotificationService < Service + include ChatMessage + + default_value_for :category, 'chat' + + prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + + validates :webhook, presence: true, url: true, if: :activated? + + def initialize_properties + # Custom serialized properties initialization + self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } + + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true + end + end + + def can_test? + valid? + end + + def supported_events + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + return unless webhook.present? + + object_kind = data[:object_kind] + + data = data.merge( + project_url: project_url, + project_name: project_name + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = get_message(object_kind, data) + + return false unless message + + channel_name = get_channel_field(object_kind).presence || channel + + opts = {} + opts[:channel] = channel_name if channel_name + opts[:username] = username if username + + notifier = Slack::Notifier.new(webhook, opts) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + end + + def event_channel_names + supported_events.map { |event| event_channel_name(event) } + end + + def event_field(event) + fields.find { |field| field[:name] == event_channel_name(event) } + end + + def global_fields + fields.reject { |field| field[:name].end_with?('channel') } + end + + def default_channel_placeholder + raise NotImplementedError + end + + private + + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + + def get_channel_field(event) + field_name = event_channel_name(event) + self.public_send(field_name) + end + + def build_event_channels + supported_events.reduce([]) do |channels, event| + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder } + end + end + + def event_channel_name(event) + "#{event}_channel" + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end +end diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb deleted file mode 100644 index d36beff5fa6e69829fac47db912b808f628e2d5a..0000000000000000000000000000000000000000 --- a/app/models/project_services/chat_service.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Base class for Chat services -# This class is not meant to be used directly, but only to inherrit from. -class ChatService < Service - default_value_for :category, 'chat' - - has_many :chat_names, foreign_key: :service_id - - def valid_token?(token) - self.respond_to?(:token) && - self.token.present? && - ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) - end - - def supported_events - [] - end - - def trigger(params) - raise NotImplementedError - end -end diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..0bc160af6042638afcd815222bc074e24854c570 --- /dev/null +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -0,0 +1,56 @@ +# Base class for Chat services +# This class is not meant to be used directly, but only to inherrit from. +class ChatSlashCommandsService < Service + default_value_for :category, 'chat' + + prop_accessor :token + + has_many :chat_names, foreign_key: :service_id, dependent: :destroy + + def valid_token?(token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + end + + def supported_events + [] + end + + def can_test? + false + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' } + ] + end + + def trigger(params) + return unless valid_token?(params[:token]) + + user = find_chat_user(params) + unless user + url = authorize_chat_name_url(params) + return presenter.authorize_chat_name(url) + end + + Gitlab::ChatCommands::Command.new(project, user, + params).execute + end + + private + + def find_chat_user(params) + ChatNames::FindUserService.new(self, params).execute + end + + def authorize_chat_name_url(params) + ChatNames::AuthorizeUserService.new(self, params).execute + end + + def presenter + Gitlab::ChatCommands::Presenter.new + end +end diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 596c00705adcbad6fa5497e0620109cf82d41761..4de0106707e644f8446a1968f11a778081d38824 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -12,15 +12,7 @@ class CiService < Service %w(push) end - def merge_request_page(iid, sha, ref) - commit_page(sha, ref) - end - - def commit_page(sha, ref) - build_page(sha, ref) - end - - # Return complete url to merge_request page + # Return complete url to build page # # Ex. # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c @@ -29,23 +21,6 @@ class CiService < Service # implement inside child end - # Return string with build status or :error symbol - # - # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' - # - # - # Ex. - # @service.merge_request_status(9, '13be4ac', 'dev') - # # => 'success' - # - # @service.merge_request_status(10, '2abe4ac', 'dev) - # # => 'running' - # - # - def merge_request_status(iid, sha, ref) - commit_status(sha, ref) - end - # Return string with build status or :error symbol # # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..ab353a1abe61c26e4aa720fda95f9eaf07e6e9e7 --- /dev/null +++ b/app/models/project_services/deployment_service.rb @@ -0,0 +1,33 @@ +# Base class for deployment services +# +# These services integrate with a deployment solution like Kubernetes/OpenShift, +# Mesosphere, etc, to provide additional features to environments. +class DeploymentService < Service + default_value_for :category, 'deployment' + + def supported_events + [] + end + + def predefined_variables + [] + end + + # Environments may have a number of terminals. Should return an array of + # hashes describing them, e.g.: + # + # [{ + # :selectors => {"a" => "b", "foo" => "bar"}, + # :url => "wss://external.example.com/exec", + # :headers => {"Authorization" => "Token xxx"}, + # :subprotocols => ["foo"], + # :ca_pem => "----BEGIN CERTIFICATE...", # optional + # :created_at => Time.now.utc + # }] + # + # Selectors should be a set of values that uniquely identify a particular + # terminal + def terminals(environment) + raise NotImplementedError + end +end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index adc78a427ee30e82b446a9c44ab1854127cb561b..4bbbebf54cba17bb64405cb1e89807b3bdc2fd6b 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,4 +1,6 @@ class DroneCiService < CiService + include ReactiveService + prop_accessor :drone_url, :token boolean_accessor :enable_ssl_verification @@ -34,14 +36,6 @@ class DroneCiService < CiService %w(push merge_request tag_push) end - def merge_request_status_path(iid, sha = nil, ref = nil) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", - "?access_token=#{token}"] - - URI.join(*url).to_s - end - def commit_status_path(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", @@ -50,54 +44,34 @@ class DroneCiService < CiService URI.join(*url).to_s end - def merge_request_status(iid, sha, ref) - response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification) - - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed - else - response["status"] - end - else - :error - end - rescue Errno::ECONNREFUSED - :error + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end - def commit_status(sha, ref) + def calculate_reactive_cache(sha, ref) response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification) - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed + status = + if response.code == 200 and response['status'] + case response['status'] + when 'killed' + :canceled + when 'failure', 'error' + # Because drone return error if some test env failed + :failed + else + response["status"] + end else - response["status"] + :error end - else - :error - end - rescue Errno::ECONNREFUSED - :error - end - def merge_request_page(iid, sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] - - URI.join(*url).to_s + { commit_status: status } + rescue Errno::ECONNREFUSED + { commit_status: :error } end - def commit_page(sha, ref) + def build_page(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}"] @@ -105,14 +79,6 @@ class DroneCiService < CiService URI.join(*url).to_s end - def commit_coverage(sha, ref) - nil - end - - def build_page(sha, ref) - commit_page(sha, ref) - end - def title 'Drone CI' end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 207bb816ad152b538729bb8fd651193048846708..bce2cdd55165ee3060e1eda1bbeaa930ae0cde22 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -85,8 +85,8 @@ class IssueTrackerService < Service def enabled_in_gitlab_config Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker + Gitlab.config.issues_tracker.values.any? && + issues_tracker end def issues_tracker diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..085125ca9dce64662005f3ca0a963699c5ddefd2 --- /dev/null +++ b/app/models/project_services/kubernetes_service.rb @@ -0,0 +1,173 @@ +class KubernetesService < DeploymentService + include Gitlab::Kubernetes + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + + # Namespace defaults to the project path, but can be overridden in case that + # is an invalid or inappropriate name + prop_accessor :namespace + + # Access to kubernetes is directly through the API + prop_accessor :api_url + + # Bearer authentication + # TODO: user/password auth, client certificates + prop_accessor :token + + # Provide a custom CA bundle for self-signed deployments + prop_accessor :ca_pem + + with_options presence: true, if: :activated? do + validates :api_url, url: true + validates :token + + validates :namespace, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message, + }, + length: 1..63 + end + + after_save :clear_reactive_cache! + + def initialize_properties + if properties.nil? + self.properties = {} + self.namespace = project.path if project.present? + end + end + + def title + 'Kubernetes' + end + + def description + 'Kubernetes / Openshift integration' + end + + def help + 'To enable terminal access to Kubernetes environments, label your ' \ + 'deployments with `app=$CI_ENVIRONMENT_SLUG`' + end + + def to_param + 'kubernetes' + end + + def fields + [ + { type: 'text', + name: 'namespace', + title: 'Kubernetes namespace', + placeholder: 'Kubernetes namespace', + }, + { type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Kubernetes API URL, like https://kube.example.com/', + }, + { type: 'text', + name: 'token', + title: 'Service token', + placeholder: 'Service token', + }, + { type: 'textarea', + name: 'ca_pem', + title: 'Custom CA bundle', + placeholder: 'Certificate Authority bundle (PEM format)', + }, + ] + end + + # Check we can connect to the Kubernetes API + def test(*args) + kubeclient = build_kubeclient! + + kubeclient.discover + { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + rescue => err + { success: false, result: err } + end + + def predefined_variables + variables = [ + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: namespace, public: true } + ] + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present? + variables + end + + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = data.fetch(:pods, nil) + filter_pods(pods, app: environment.slug). + flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. + map { |terminal| add_terminal_auth(terminal, token, ca_pem) } + end + end + + # Caches all pods in the namespace so other calls don't need to block on + # network access. + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? + + kubeclient = build_kubeclient! + + # Store as hashes, rather than as third-party types + pods = begin + kubeclient.get_pods(namespace: namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + # We may want to cache extra things in the future + { pods: pods } + end + + private + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && namespace && token + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + def kubeclient_auth_options + { bearer_token: token } + end + + def join_api_url(*parts) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [ prefix, *parts ].join("/") + + url.to_s + end +end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee8a0b5527561b2f66793ba3ded8b7ec47da7d26 --- /dev/null +++ b/app/models/project_services/mattermost_service.rb @@ -0,0 +1,41 @@ +class MattermostService < ChatNotificationService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def to_param + 'mattermost' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.<br /> + To set up this service: + <ol> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> + <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel_placeholder + "#town-square" + end +end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 33431f41dc2344a2f196b5fb1b6a294b1f4ccff2..2cb481182d7073fb7461481661a3416e94f0ffa5 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,4 +1,4 @@ -class MattermostSlashCommandsService < ChatService +class MattermostSlashCommandsService < ChatSlashCommandsService include TriggersHelper prop_accessor :token @@ -19,31 +19,33 @@ class MattermostSlashCommandsService < ChatService 'mattermost_slash_commands' end - def fields - [ - { type: 'text', name: 'token', placeholder: '' } - ] - end - - def trigger(params) - return nil unless valid_token?(params[:token]) + def configure(user, params) + token = Mattermost::Command.new(user). + create(command(params)) - user = find_chat_user(params) - unless user - url = authorize_chat_name_url(params) - return Mattermost::Presenter.authorize_chat_name(url) - end + update(active: true, token: token) if token + rescue Mattermost::Error => e + [false, e.message] + end - Gitlab::ChatCommands::Command.new(project, user, params).execute + def list_teams(user) + Mattermost::Team.new(user).all + rescue Mattermost::Error => e + [[], e.message] end private - def find_chat_user(params) - ChatNames::FindUserService.new(self, params).execute - end - - def authorize_chat_name_url(params) - ChatNames::AuthorizeUserService.new(self, params).execute + def command(params) + pretty_project_name = project.name_with_namespace + + params.merge( + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{pretty_project_name}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{pretty_project_name}", + display_name: "GitLab / #{pretty_project_name}", + method: 'P', + username: 'GitLab') end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index e1b937817f4649d7bfd4adbdd10665da35b023ef..76d233a3cca24dd2930f53b65c5b3e93196eb888 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,25 +1,10 @@ -class SlackService < Service - prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines - validates :webhook, presence: true, url: true, if: :activated? - - def initialize_properties - # Custom serialized properties initialization - self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } - - if properties.nil? - self.properties = {} - self.notify_only_broken_builds = true - self.notify_only_broken_pipelines = true - end - end - +class SlackService < ChatNotificationService def title - 'Slack' + 'Slack notifications' end def description - 'A team communication tool for the 21st century' + 'Receive event notifications in Slack' end def to_param @@ -27,150 +12,29 @@ class SlackService < Service end def help - 'This service sends notifications to your Slack channel.<br/> - To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel, - and enter the Webhook URL below.' + 'This service sends notifications about projects events to Slack channels.<br /> + To setup this service: + <ol> + <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> + <li>Paste the <strong>Webhook URL</strong> into the field below. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' end def fields - default_fields = - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: "#general" }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - default_fields + build_event_channels end - def supported_events - %w[push issue confidential_issue merge_request note tag_push - build pipeline wiki_page] - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - return unless webhook.present? - - object_kind = data[:object_kind] - - data = data.merge( - project_url: project_url, - project_name: project_name - ) - - # WebHook events often have an 'update' event that follows a 'open' or - # 'close' action. Ignore update events for now to prevent duplicate - # messages from arriving. - - message = get_message(object_kind, data) - - if message - opt = {} - - event_channel = get_channel_field(object_kind) || channel - - opt[:channel] = event_channel if event_channel - opt[:username] = username if username - - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - - true - else - false - end - end - - def event_channel_names - supported_events.map { |event| event_channel_name(event) } - end - - def event_field(event) - fields.find { |field| field[:name] == event_channel_name(event) } + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] end - def global_fields - fields.reject { |field| field[:name].end_with?('channel') } - end - - private - - def get_message(object_kind, data) - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - end - - def get_channel_field(event) - field_name = event_channel_name(event) - self.public_send(field_name) - end - - def build_event_channels - supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } - end - end - - def event_channel_name(event) - "#{event}_channel" - end - - def project_name - project.name_with_namespace.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def is_update?(data) - data[:object_attributes][:action] == 'update' - end - - def should_build_be_notified?(data) - case data[:commit][:status] - when 'success' - !notify_only_broken_builds? - when 'failed' - true - else - false - end - end - - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end + def default_channel_placeholder + "#general" end end - -require "slack_service/issue_message" -require "slack_service/push_message" -require "slack_service/merge_message" -require "slack_service/note_message" -require "slack_service/build_message" -require "slack_service/pipeline_message" -require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..5a7cc0fb329799c90d25c92f6551f2b61cf7aa91 --- /dev/null +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -0,0 +1,28 @@ +class SlackSlashCommandsService < ChatSlashCommandsService + include TriggersHelper + + def title + 'Slack Command' + end + + def description + "Perform common operations on GitLab in Slack" + end + + def to_param + 'slack_slash_commands' + end + + def trigger(params) + # Format messages to be Slack-compatible + super.tap do |result| + result[:text] = format(result[:text]) if result.is_a?(Hash) + end + end + + private + + def format(text) + Slack::Notifier::LinkFormatter.format(text) if text + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a4a967c9bc94b02e6048a2dce870f8fb5f8a9a99..6726082048fa579298792a824fbb87ee819410b0 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,4 +1,6 @@ class TeamcityService < CiService + include ReactiveService + prop_accessor :teamcity_url, :build_type, :username, :password validates :teamcity_url, presence: true, url: true, if: :activated? @@ -61,43 +63,18 @@ class TeamcityService < CiService ] end - def build_info(sha) - @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code - - if @response.code != 200 - # If actual build link can't be determined, - # send user to build summary page. - build_url("viewLog.html?buildTypeId=#{build_type}") - else - # If actual build link is available, go to build result page. - built_id = @response['build']['id'] - build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") - end + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + end - status = if @response.code == 404 - 'Pending' - else - @response['build']['status'] - end + def calculate_reactive_cache(sha, ref) + response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - if status.include?('SUCCESS') - 'success' - elsif status.include?('FAILURE') - 'failed' - elsif status.include?('Pending') - 'pending' - else - :error - end + { build_page: read_build_page(response), commit_status: read_commit_status(response) } end def execute(data) @@ -122,6 +99,40 @@ class TeamcityService < CiService private + def read_build_page(response) + if response.code != 200 + # If actual build link can't be determined, + # send user to build summary page. + build_url("viewLog.html?buildTypeId=#{build_type}") + else + # If actual build link is available, go to build result page. + built_id = response['build']['id'] + build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") + end + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'Pending' + else + response['build']['status'] + end + + return :error unless status.present? + + if status.include?('SUCCESS') + 'success' + elsif status.include?('FAILURE') + 'failed' + elsif status.include?('Pending') + 'pending' + else + :error + end + end + def build_url(path) URI.join("#{teamcity_url}/", path).to_s end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb new file mode 100644 index 0000000000000000000000000000000000000000..06abd4065239a8c60de134f463042cea1bb1642a --- /dev/null +++ b/app/models/project_statistics.rb @@ -0,0 +1,44 @@ +class ProjectStatistics < ActiveRecord::Base + belongs_to :project + belongs_to :namespace + + before_save :update_storage_size + + STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size] + STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS + + def total_repository_size + repository_size + lfs_objects_size + end + + def refresh!(only: nil) + STATISTICS_COLUMNS.each do |column, generator| + if only.blank? || only.include?(column) + public_send("update_#{column}") + end + end + + save! + end + + def update_commit_count + self.commit_count = project.repository.commit_count + end + + # Repository#size needs to be converted from MB to Byte. + def update_repository_size + self.repository_size = project.repository.size * 1.megabyte + end + + def update_lfs_objects_size + self.lfs_objects_size = project.lfs_objects.sum(:size) + end + + def update_build_artifacts_size + self.build_artifacts_size = project.builds.sum(:artifacts_size) + end + + def update_storage_size + self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute)) + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 1ccabdb7c1f16fb35219ba64e85935f14cabfb2c..43dba86e5ed4afa212b4c4c741a60df0ea235b0a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -439,6 +439,11 @@ class Repository expire_content_cache end + # Runs code after the HEAD of a repository is changed. + def after_change_head + expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) + end + # Runs code after a repository has been forked/imported. def after_import expire_content_cache @@ -654,11 +659,19 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - sha = Gitlab::Popen.popen(args, path_to_repo).first.strip + sha = last_commit_id_for_path(sha, path) commit(sha) end + def last_commit_id_for_path(sha, path) + key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" + + cache.fetch(key) do + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) + Gitlab::Popen.popen(args, path_to_repo).first.strip + end + end + def next_branch(name, opts = {}) branch_ids = self.branch_names.map do |n| next 1 if n == name diff --git a/app/models/route.rb b/app/models/route.rb index d40214b9da6ac93904ce7ebbb4b0352d353ae511..caf596efa79627bca90455150e15dca27be730e2 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -13,7 +13,7 @@ class Route < ActiveRecord::Base def rename_children # We update each row separately because MySQL does not have regexp_replace. # rubocop:disable Rails/FindEach - Route.where('path LIKE ?', "#{path_was}%").each do |route| + Route.where('path LIKE ?', "#{path_was}/%").each do |route| # Note that update column skips validation and callbacks. # We need this to avoid recursive call of rename_children method route.update_column(:path, route.path.sub(path_was, path)) diff --git a/app/models/service.rb b/app/models/service.rb index 0c36acfc1b72bc259dfac81690089959071fbec8..19ef3ba9c2382a17442f7a790332cc7b42024d53 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -214,11 +214,14 @@ class Service < ActiveRecord::Base hipchat irker jira + kubernetes mattermost_slash_commands + mattermost pipelines_email pivotaltracker pushover redmine + slack_slash_commands slack teamcity ] diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 98ccf5f331fb32e03b64996dfa1b1eb9c3e4356e..771a73505562bdd5a359fe606360c56ee2cd1fc0 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -64,11 +64,11 @@ class Snippet < ActiveRecord::Base @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{id}" if project.present? - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" else reference end diff --git a/app/models/timelog.rb b/app/models/timelog.rb new file mode 100644 index 0000000000000000000000000000000000000000..f768c4e3da5da4531063367de6fea5ea3ebab0d3 --- /dev/null +++ b/app/models/timelog.rb @@ -0,0 +1,6 @@ +class Timelog < ActiveRecord::Base + validates :time_spent, :user, presence: true + + belongs_to :trackable, polymorphic: true + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb index 1bd28203523f460de46f88d6a04d8922b6b773c1..06dd98a318858f85a3a845a73874efa0b99cd3fa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,7 +73,7 @@ class User < ActiveRecord::Base has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy has_many :starred_projects, through: :users_star_projects, source: :project - has_many :project_authorizations, dependent: :destroy + has_many :project_authorizations has_many :authorized_projects, through: :project_authorizations, source: :project has_many :snippets, dependent: :destroy, foreign_key: :author_id @@ -99,6 +99,7 @@ class User < ActiveRecord::Base # # Note: devise :validatable above adds validations for :email and :password validates :name, presence: true + validates_confirmation_of :email validates :notification_email, presence: true validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true @@ -178,6 +179,8 @@ class User < ActiveRecord::Base scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } + scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) } + scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) } def self.with_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). @@ -205,8 +208,8 @@ class User < ActiveRecord::Base def sort(method) case method.to_s - when 'recent_sign_in' then reorder(last_sign_in_at: :desc) - when 'oldest_sign_in' then reorder(last_sign_in_at: :asc) + when 'recent_sign_in' then order_recent_sign_in + when 'oldest_sign_in' then order_oldest_sign_in else order_by(method) end @@ -309,10 +312,6 @@ class User < ActiveRecord::Base find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) end - def build_user(attrs = {}) - User.new(attrs) - end - def reference_prefix '@' end @@ -334,7 +333,7 @@ class User < ActiveRecord::Base username end - def to_reference(_from_project = nil, _target_project = nil) + def to_reference(_from_project = nil, target_project: nil, full: nil) "#{self.class.reference_prefix}#{username}" end @@ -390,7 +389,7 @@ class User < ActiveRecord::Base def namespace_uniq # Return early if username already failed the first uniqueness validation return if errors.key?(:username) && - errors[:username].include?('has already been taken') + errors[:username].include?('has already been taken') existing_namespace = Namespace.by_path(username) if existing_namespace && existing_namespace != namespace @@ -441,22 +440,16 @@ class User < ActiveRecord::Base end def refresh_authorized_projects - transaction do - project_authorizations.delete_all - - # project_authorizations_union can return multiple records for the same - # project/user with different access_level so we take row with the maximum - # access_level - project_authorizations.connection.execute <<-SQL - INSERT INTO project_authorizations (user_id, project_id, access_level) - SELECT user_id, project_id, MAX(access_level) AS access_level - FROM (#{project_authorizations_union.to_sql}) sub - GROUP BY user_id, project_id - SQL - - unless authorized_projects_populated - update_column(:authorized_projects_populated, true) - end + Users::RefreshAuthorizedProjectsService.new(self).execute + end + + def remove_project_authorizations(project_ids) + project_authorizations.where(project_id: project_ids).delete_all + end + + def set_authorized_projects_column + unless authorized_projects_populated + update_column(:authorized_projects_populated, true) end end @@ -903,18 +896,6 @@ class User < ActiveRecord::Base private - # Returns a union query of projects that the user is authorized to access - def project_authorizations_union - relations = [ - personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), - groups_projects.select_for_project_authorization, - projects.select_for_project_authorization, - groups.joins(:shared_projects).select_for_project_authorization - ] - - Gitlab::SQL::Union.new(relations) - end - def ci_projects_union scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } groups = groups_projects.where(members: scope) diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 6f943feb2a72690cc51806eb5b98a2b3394cb8eb..0be6e1136558be7fa339bcb63f5a083d842045a3 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy return unless @user globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) - member = @subject.users.include?(@user) + member = @subject.users_with_parents.include?(@user) owner = @user.admin? || @subject.has_owner?(@user) master = owner || @subject.has_master?(@user) diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 83847466ee23ef3e35ce1b79a31f400961c8efd0..5326061bd077f804317ea0c4f2f94647cfa9739b 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -12,7 +12,7 @@ class NotePolicy < BasePolicy end if @subject.for_merge_request? && - @subject.noteable.author == @user + @subject.noteable.author == @user can! :resolve_note end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index d5aadfce76a8267e4842858c3ffaca0567f157ba..71ef89019327ca8fdb5248c8036c6d9dc539a732 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy team_access!(user) owner = project.owner == user || - (project.group && project.group.has_owner?(user)) + (project.group && project.group.has_owner?(user)) owner_access! if user.admin? || owner team_member_owner_access! if owner @@ -13,7 +13,7 @@ class ProjectPolicy < BasePolicy public_access! if project.request_access_enabled && - !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) + !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access end end @@ -171,9 +171,7 @@ class ProjectPolicy < BasePolicy def disabled_features! repository_enabled = project.feature_available?(:repository, user) - unless project.feature_available?(:issues, user) - cannot!(*named_abilities(:issue)) - end + block_issues_abilities unless project.feature_available?(:merge_requests, user) && repository_enabled cannot!(*named_abilities(:merge_request)) @@ -244,10 +242,19 @@ class ProjectPolicy < BasePolicy def project_group_member?(user) project.group && - ( - project.group.members.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) + ( + project.group.members_with_parents.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end + + def block_issues_abilities + unless project.feature_available?(:issues, user) + cannot! :read_issue if project.default_issues_tracker? + cannot! :create_issue + cannot! :update_issue + cannot! :admin_issue + end end def named_abilities(name) diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..a559d0850c48b35dd84769e8795f214a55e7fda0 --- /dev/null +++ b/app/serializers/analytics_stage_entity.rb @@ -0,0 +1,10 @@ +class AnalyticsStageEntity < Grape::Entity + include EntityDateHelper + + expose :title + expose :description + + expose :median, as: :value do |stage| + stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil + end +end diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..613cf6874d866750d4497ae0630797008779eece --- /dev/null +++ b/app/serializers/analytics_stage_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsStageSerializer < BaseSerializer + entity AnalyticsStageEntity +end diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..91803ec07f5ffd2cd2d5a156f4df237068b93d05 --- /dev/null +++ b/app/serializers/analytics_summary_entity.rb @@ -0,0 +1,7 @@ +class AnalyticsSummaryEntity < Grape::Entity + expose :value, safe: true + + expose :title do |object| + object.title.pluralize(object.value) + end +end diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..c87a24aa47c0df78f7bd40709cd151497abfc47e --- /dev/null +++ b/app/serializers/analytics_summary_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsSummarySerializer < BaseSerializer + entity AnalyticsSummaryEntity +end diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..184f5fd4b52490c89d8876227bf52e20017bd001 --- /dev/null +++ b/app/serializers/build_action_entity.rb @@ -0,0 +1,14 @@ +class BuildActionEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :path do |build| + play_namespace_project_build_path( + build.project.namespace, + build.project, + build) + end +end diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..8b643d8e783fa88732432c2b0d77a3d601a98e79 --- /dev/null +++ b/app/serializers/build_artifact_entity.rb @@ -0,0 +1,14 @@ +class BuildArtifactEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :path do |build| + download_namespace_project_build_artifacts_path( + build.project.namespace, + build.project, + build) + end +end diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb index acc20f6dc52f69ba205454f0491361cad243bcf1..31763955f978d606c1c90ef2d9913407fb519d4a 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -3,17 +3,21 @@ class CommitEntity < API::Entities::RepoCommit expose :author, using: UserEntity + expose :author_gravatar_url do |commit| + GravatarService.new.execute(commit.author_email) + end + expose :commit_url do |commit| - namespace_project_tree_url( + namespace_project_commit_url( request.project.namespace, request.project, - id: commit.id) + commit) end expose :commit_path do |commit| - namespace_project_tree_path( + namespace_project_commit_path( request.project.namespace, request.project, - id: commit.id) + commit) end end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 7e0fc9c071e84c22b378e33db3c27c65f9380167..5d15eb8d3d33165a5a855b6fede58ccb6b923339 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -23,5 +23,13 @@ class EnvironmentEntity < Grape::Entity environment) end + expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| + can?(request.user, :admin_environment, environment.project) && + terminal_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + expose :created_at, :updated_at end diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 17c9160cb197b5f4884ad2827075a08d48554d44..29aecb50849fce40bd630b01c80763e8a7ce8cbe 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity expose :created_at expose :updated_at expose :deleted_at + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..d04a4990cb0861990204e6f22efc356cbaf855ec --- /dev/null +++ b/app/serializers/pipeline_entity.rb @@ -0,0 +1,83 @@ +class PipelineEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :user, using: UserEntity + + expose :path do |pipeline| + namespace_project_pipeline_path( + pipeline.project.namespace, + pipeline.project, + pipeline) + end + + expose :details do + expose :status do |pipeline, options| + StatusEntity.represent( + pipeline.detailed_status(request.user), + options) + end + + expose :duration + expose :finished_at + expose :stages, using: StageEntity + expose :artifacts, using: BuildArtifactEntity + expose :manual_actions, using: BuildActionEntity + end + + expose :flags do + expose :latest?, as: :latest + expose :triggered?, as: :triggered + expose :stuck?, as: :stuck + expose :has_yaml_errors?, as: :yaml_errors + expose :can_retry?, as: :retryable + expose :can_cancel?, as: :cancelable + end + + expose :ref do + expose :name do |pipeline| + pipeline.ref + end + + expose :path do |pipeline| + namespace_project_tree_path( + pipeline.project.namespace, + pipeline.project, + id: pipeline.ref) + end + + expose :tag?, as: :tag + expose :branch?, as: :branch + end + + expose :commit, using: CommitEntity + expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? } + + expose :retry_path, if: proc { can_retry? } do |pipeline| + retry_namespace_project_pipeline_path(pipeline.project.namespace, + pipeline.project, + pipeline.id) + end + + expose :cancel_path, if: proc { can_cancel? } do |pipeline| + cancel_namespace_project_pipeline_path(pipeline.project.namespace, + pipeline.project, + pipeline.id) + end + + expose :created_at, :updated_at + + private + + alias_method :pipeline, :object + + def can_retry? + pipeline.retryable? && + can?(request.user, :update_pipeline, pipeline) + end + + def can_cancel? + pipeline.cancelable? && + can?(request.user, :update_pipeline, pipeline) + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..cfa86cc2553f228f816f14e66d3515ee23796d08 --- /dev/null +++ b/app/serializers/pipeline_serializer.rb @@ -0,0 +1,40 @@ +class PipelineSerializer < BaseSerializer + entity PipelineEntity + class InvalidResourceError < StandardError; end + include API::Helpers::Pagination + Struct.new('Pagination', :request, :response) + + def represent(resource, opts = {}) + if paginated? + raise InvalidResourceError unless resource.respond_to?(:page) + + super(paginate(resource.includes(project: :namespace)), opts) + else + super(resource, opts) + end + end + + def paginated? + defined?(@pagination) + end + + def with_pagination(request, response) + tap { @pagination = Struct::Pagination.new(request, response) } + end + + private + + # Methods needed by `API::Helpers::Pagination` + # + def params + @pagination.request.query_parameters + end + + def request + @pagination.request + end + + def header(header, value) + @pagination.response.headers[header] = value + end +end diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb index ff8c1142abc114809dc57354197d7b46d602a35c..3039014aaaa6af358a71e473f2c4d8ee39ada2f3 100644 --- a/app/serializers/request_aware_entity.rb +++ b/app/serializers/request_aware_entity.rb @@ -2,10 +2,11 @@ module RequestAwareEntity extend ActiveSupport::Concern included do - include Gitlab::Routing.url_helpers + include Gitlab::Routing + include Gitlab::Allowable end def request - @options.fetch(:request) + options.fetch(:request) end end diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..7a047bdc712388a5d6f96899d467ecad13c84d04 --- /dev/null +++ b/app/serializers/stage_entity.rb @@ -0,0 +1,38 @@ +class StageEntity < Grape::Entity + include RequestAwareEntity + + expose :name + + expose :title do |stage| + "#{stage.name}: #{detailed_status.label}" + end + + expose :detailed_status, + as: :status, + with: StatusEntity + + expose :path do |stage| + namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline, + anchor: stage.name) + end + + expose :dropdown_path do |stage| + stage_namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline, + stage: stage.name, + format: :json) + end + + private + + alias_method :stage, :object + + def detailed_status + stage.detailed_status(request.user) + end +end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..47066bebfb11075658c0888d381590d4bce27bf5 --- /dev/null +++ b/app/serializers/status_entity.rb @@ -0,0 +1,8 @@ +class StatusEntity < Grape::Entity + include RequestAwareEntity + + expose :icon, :text, :label, :group + + expose :has_details?, as: :has_details + expose :details_path +end diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..ddaaed90e5befeec1f27f45c155bc8c3ca583a10 --- /dev/null +++ b/app/services/access_token_validation_service.rb @@ -0,0 +1,32 @@ +AccessTokenValidationService = Struct.new(:token) do + # Results: + VALID = :valid + EXPIRED = :expired + REVOKED = :revoked + INSUFFICIENT_SCOPE = :insufficient_scope + + def validate(scopes: []) + if token.expired? + return EXPIRED + + elsif token.revoked? + return REVOKED + + elsif !self.include_any_scope?(scopes) + return INSUFFICIENT_SCOPE + + else + return VALID + end + end + + # True if the token's scope contains any of the passed scopes. + def include_any_scope?(scopes) + if scopes.blank? + true + else + # Check whether the token is allowed access to any of the required scopes. + Set.new(scopes).intersection(Set.new(token.scopes)).present? + end + end +end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb index 005014fa1ded2e94b350f06aad0ec2bf1613106f..b7da3f8e7eb04dbdf540608ded89e17119157c8d 100644 --- a/app/services/ci/create_pipeline_builds_service.rb +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -10,18 +10,29 @@ module Ci end end + def project + pipeline.project + end + private def create_build(build_attributes) build_attributes = build_attributes.merge( pipeline: pipeline, - project: pipeline.project, + project: project, ref: pipeline.ref, tag: pipeline.tag, user: current_user, trigger_request: trigger_request ) - pipeline.builds.create(build_attributes) + build = pipeline.builds.create(build_attributes) + + # Create the environment before the build starts. This sets its slug and + # makes it available as an environment variable + project.environments.find_or_create_by(name: build.expanded_environment_name) if + build.has_environment? + + build end def new_builds diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 75d847d5bee9043d35ce0ed33a39163c064f12d0..240ddabec36cf73e5ea64dfab68ee26c7a87acbf 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,13 +1,13 @@ module Ci class ImageForBuildService def execute(project, opts) - sha = opts[:sha] || ref_sha(project, opts[:ref]) - + ref = opts[:ref] + sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(pipelines.status) + image_name = image_for_status(pipelines.latest_status(ref)) image_path = Rails.root.join('public/ci', image_name) + OpenStruct.new(path: image_path, name: image_name) end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 185556c12cccb0f6cfb04eb7b0b5a2e2c98c60fb..dbe2fda27b5192e31165c71a23c56e2267e21e51 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -3,6 +3,9 @@ class GitPushService < BaseService include Gitlab::CurrentSettings include Gitlab::Access + # The N most recent commits to process in a single push payload. + PROCESS_COMMIT_LIMIT = 100 + # This method will be called after each git update # and only if the provided user and project are present in GitLab. # @@ -74,7 +77,17 @@ class GitPushService < BaseService types = [] end - ProjectCacheWorker.perform_async(@project.id, types) + ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size]) + end + + # Schedules processing of commit messages. + def process_commit_messages + default = is_default_branch? + + push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| + ProcessCommitWorker. + perform_async(project.id, current_user.id, commit.to_hash, default) + end end protected @@ -128,17 +141,6 @@ class GitPushService < BaseService end end - # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, - # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. - def process_commit_messages - default = is_default_branch? - - @push_commits.each do |commit| - ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.to_hash, default) - end - end - def build_push_data @push_data ||= Gitlab::DataBuilder::Push.build( @project, diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 20a4445bddf2334b54be1388b4aec5a1b44a4ec3..96432837481e5e3a577851395eeba0b95b595989 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -12,7 +12,7 @@ class GitTagPushService < BaseService project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) Ci::CreatePipelineService.new(project, current_user, @push_data).execute - ProjectCacheWorker.perform_async(project.id) + ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) true end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 2bccd584dde1e4c5eb023ba0d5248bc547db862d..febeb661fb5f3bb5622a5c06591e19d223dde4df 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -12,6 +12,13 @@ module Groups return @group end + if @group.parent && !can?(current_user, :admin_group, @group.parent) + @group.parent = nil + @group.errors.add(:parent_id, 'manage access required to create subgroup') + + return @group + end + @group.name ||= @group.path.dup @group.save @group.add_owner(current_user) diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 99ad12b1003c3a068099055666d16e63110e9046..4e878ec556a2e77f2ac571e6c45efa938f0e91aa 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -5,7 +5,7 @@ module Groups new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level unless can?(current_user, :change_visibility_level, group) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(group, new_visibility) return group @@ -14,7 +14,13 @@ module Groups group.assign_attributes(params) - group.save + begin + group.save + rescue Gitlab::UpdatePathError => e + group.errors.add(:base, e.message) + + false + end end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b5f63cc5a1a0592d2e68dd988a94c86bf8e0af81..5f3ced49665ca1a702f23a7d1b5c6a37026754bc 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -36,14 +36,18 @@ class IssuableBaseService < BaseService end end - def filter_params(issuable_ability_name = :issue) - filter_assignee - filter_milestone - filter_labels + def create_time_estimate_note(issuable) + SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) + end - ability = :"admin_#{issuable_ability_name}" + def create_time_spent_note(issuable) + SystemNoteService.change_time_spent(issuable, issuable.project, current_user) + end + + def filter_params(issuable) + ability_name = :"admin_#{issuable.to_ability_name}" - unless can?(current_user, ability, project) + unless can?(current_user, ability_name, project) params.delete(:milestone_id) params.delete(:labels) params.delete(:add_label_ids) @@ -52,14 +56,35 @@ class IssuableBaseService < BaseService params.delete(:assignee_id) params.delete(:due_date) end + + filter_assignee(issuable) + filter_milestone + filter_labels end - def filter_assignee - if params[:assignee_id] == IssuableFinder::NONE - params[:assignee_id] = '' + def filter_assignee(issuable) + return unless params[:assignee_id].present? + + assignee_id = params[:assignee_id] + + if assignee_id.to_s == IssuableFinder::NONE + params[:assignee_id] = "" + else + params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id) end end + def assignee_can_read?(issuable, assignee_id) + new_assignee = User.find_by_id(assignee_id) + + return false unless new_assignee.present? + + ability_name = :"read_#{issuable.to_ability_name}" + resource = issuable.persisted? ? issuable : project + + can?(new_assignee, ability_name, resource) + end + def filter_milestone milestone_id = params[:milestone_id] return unless milestone_id @@ -138,7 +163,7 @@ class IssuableBaseService < BaseService def create(issuable) merge_slash_commands_into_params!(issuable) - filter_params + filter_params(issuable) params.delete(:state_event) params[:author] ||= current_user @@ -180,11 +205,12 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) - filter_params + filter_params(issuable) old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a - params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids) + label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) + params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) if params.present? && update_issuable(issuable, params) # We do not touch as it will affect a update on updated_at field @@ -201,6 +227,10 @@ class IssuableBaseService < BaseService issuable end + def labels_changing?(old_label_ids, new_label_ids) + old_label_ids.sort != new_label_ids.sort + end + def change_state(issuable) case params.delete(:state_event) when 'reopen' @@ -250,6 +280,14 @@ class IssuableBaseService < BaseService create_task_status_note(issuable) end + if issuable.previous_changes.include?('time_estimate') + create_time_estimate_note(issuable) + end + + if issuable.time_spent? + create_time_spent_note(issuable) + end + create_labels_note(issuable, old_labels) if issuable.labels != old_labels end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 742e834df97d6f5304d2736661d01f4bdb7e5e55..35af867a09824114089bad6ffcb73188349dde77 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -17,10 +17,6 @@ module Issues private - def filter_params - super(:issue) - end - def execute_hooks(issue, action = 'open') issue_data = hook_data(issue, action) hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index a2111b3806ba7fbc710315feb762dbe68e389d76..78cbf94ec69c03962ed545cf5d5268f689fe1eb1 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -10,7 +10,7 @@ module Issues end if issue.previous_changes.include?('title') || - issue.previous_changes.include?('description') + issue.previous_changes.include?('description') todo_service.update_issue(issue, current_user) end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 800fd39c42490a9ca025d68e6a0e9e4c616cde84..70e25956dc7140518d0b3aaaf235004d9bbd0c15 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,10 +38,6 @@ module MergeRequests private - def filter_params - super(:merge_request) - end - def merge_requests_for(branch) origin_merge_requests = @project.origin_merge_requests .opened.where(source_branch: branch).to_a diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index bebfca7537bf25caa22e1d7e443c5af11a5ed2d0..6a7393a9921728a1dcc04e72a1c9c9bf5da915ee 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -42,7 +42,7 @@ module MergeRequests end if merge_request.source_project == merge_request.target_project && - merge_request.target_branch == merge_request.source_branch + merge_request.target_branch == merge_request.source_branch messages << 'You must select different branches' end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index fda0da19d87af273c0c394a0fc95c23b68a7b147..3cb9aae83f6e6895bfe908ebeec95c9e35cf143f 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -7,6 +7,8 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + merge_from_slash_command(merge_request) if params[:merge] + if merge_request.closed_without_fork? params.except!(:target_branch, :force_remove_source_branch) end @@ -25,7 +27,7 @@ module MergeRequests end if merge_request.previous_changes.include?('title') || - merge_request.previous_changes.include?('description') + merge_request.previous_changes.include?('description') todo_service.update_merge_request(merge_request, current_user) end @@ -69,6 +71,19 @@ module MergeRequests end end + def merge_from_slash_command(merge_request) + last_diff_sha = params.delete(:merge) + return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha) + + merge_request.update(merge_error: nil) + + if merge_request.head_pipeline && merge_request.head_pipeline.active? + MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request) + else + MergeWorker.perform_async(merge_request.id, current_user.id, {}) + end + end + def reopen_service MergeRequests::ReopenService end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index d75592e31f300aec13f4c459eadaeeffddb451a9..cdd765c85eb66b0470b8bb555384a9b59d3dbe88 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -1,6 +1,8 @@ module Notes class CreateService < BaseService def execute + merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) + note = project.notes.new(params) note.author = current_user note.system = false @@ -19,7 +21,8 @@ module Notes slash_commands_service = SlashCommandsService.new(project, current_user) if slash_commands_service.supported?(note) - content, command_params = slash_commands_service.extract_commands(note) + options = { merge_request_diff_head_sha: merge_request_diff_head_sha } + content, command_params = slash_commands_service.extract_commands(note, options) only_commands = content.empty? @@ -41,7 +44,7 @@ module Notes # We must add the error after we call #save because errors are reset # when #save is called if only_commands - note.errors.add(:commands_only, 'Your commands have been executed!') + note.errors.add(:commands_only, 'Commands applied') end note.commands_changes = command_params.keys diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb index 2edbd39a9e7effa4787e759a5e79827301399c02..aaea9717fc478d8e3f25283ca67e9f032e6f6f5a 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/slash_commands_service.rb @@ -19,10 +19,10 @@ module Notes self.class.supported?(note, current_user) end - def extract_commands(note) + def extract_commands(note, options = {}) return [note.note, {}] unless supported?(note) - SlashCommands::InterpretService.new(project, current_user). + SlashCommands::InterpretService.new(project, current_user, options). execute(note.note, note.noteable) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 9a7af5730d2b3f5a43f821e6084c00e67017d1af..c3b61e68eab6da4163aad842b00e0832a8193e1f 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -591,7 +591,10 @@ class NotificationService custom_action = build_custom_key(action, target) recipients = target.participants(current_user) - recipients = add_project_watchers(recipients, project) + + unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + recipients = add_project_watchers(recipients, project) + end recipients = add_custom_notifications(recipients, project, custom_action) recipients = reject_mention_users(recipients, project) diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb deleted file mode 100644 index 264fdccde8fdf2e286fabf274ccdd8e339daa9cb..0000000000000000000000000000000000000000 --- a/app/services/oauth2/access_token_validation_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Oauth2::AccessTokenValidationService - # Results: - VALID = :valid - EXPIRED = :expired - REVOKED = :revoked - INSUFFICIENT_SCOPE = :insufficient_scope - - class << self - def validate(token, scopes: []) - if token.expired? - return EXPIRED - - elsif token.revoked? - return REVOKED - - elsif !self.sufficient_scope?(token, scopes) - return INSUFFICIENT_SCOPE - - else - return VALID - end - end - - protected - - # True if the token's scope is a superset of required scopes, - # or the required scopes is empty. - def sufficient_scope?(token, scopes) - if scopes.blank? - # if no any scopes required, the scopes of token is sufficient. - return true - else - # If there are scopes required, then check whether - # the set of authorized scopes is a superset of the set of required scopes - required_scopes = Set.new(scopes) - authorized_scopes = Set.new(token.scopes) - - return authorized_scopes >= required_scopes - end - end - end -end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index d7221fe993c145059191eabe78eb43b1d18b8087..cd230528743f9bcdef5642afa05e32c7c68e06bc 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -4,15 +4,6 @@ module Projects class Error < StandardError; end - ALLOWED_TYPES = [ - 'bitbucket', - 'fogbugz', - 'gitlab', - 'github', - 'google_code', - 'gitlab_project' - ] - def execute add_repository_to_project unless project.gitlab_project_import? @@ -64,14 +55,11 @@ module Projects end def has_importer? - ALLOWED_TYPES.include?(project.import_type) + Gitlab::ImportSources.importer_names.include?(project.import_type) end def importer - return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? - - class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" - class_name.constantize.new(project) + Gitlab::ImportSources.importer(project.import_type).new(project) end def unknown_url? diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 6040391fd947dedeb3fff66000eb0defab11042e..96c363c8d1a989d99ca379d856b9cee17303e1d9 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -36,7 +36,7 @@ module Projects def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url } + { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url } end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 921ca6748d3c6cd9dbb10298bbeed6c3e6ebf3d1..842e23eb6b60653e47171348844a3fce4068db04 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -6,10 +6,10 @@ module Projects if new_visibility && new_visibility.to_i != project.visibility_level unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(project, new_visibility) - return project + return error('Visibility level unallowed') end end @@ -23,6 +23,10 @@ module Projects if project.previous_changes.include?('path') project.rename_repo end + + success + else + error('Project could not be updated') end end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index d75c5b1800e5f3049495fba08e87dad6b1ec27fc..3566a8ba92f025f09bd568df87253f46a4c5e732 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -2,7 +2,7 @@ module SlashCommands class InterpretService < BaseService include Gitlab::SlashCommands::Dsl - attr_reader :issuable + attr_reader :issuable, :options # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. @@ -13,7 +13,8 @@ module SlashCommands opts = { issuable: issuable, current_user: current_user, - project: project + project: project, + params: params } content, commands = extractor.extract_commands(content, opts) @@ -58,6 +59,17 @@ module SlashCommands @updates[:state_event] = 'reopen' end + desc 'Merge (when build succeeds)' + condition do + last_diff_sha = params && params[:merge_request_diff_head_sha] + issuable.is_a?(MergeRequest) && + issuable.persisted? && + issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) + end + command :merge do + @updates[:merge] = params[:merge_request_diff_head_sha] + end + desc 'Change title' params '<New title>' condition do @@ -243,6 +255,50 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end + desc 'Set time estimate' + params '<1w 3d 2h 14m>' + condition do + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :estimate do |raw_duration| + time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration) + + if time_estimate + @updates[:time_estimate] = time_estimate + end + end + + desc 'Add or substract spent time' + params '<1h 30m | -1h 30m>' + condition do + current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :spend do |raw_duration| + time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration) + + if time_spent + @updates[:spend_time] = { duration: time_spent, user: current_user } + end + end + + desc 'Remove time estimate' + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :remove_estimate do + @updates[:time_estimate] = 0 + end + + desc 'Remove spent time' + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :remove_time_spent do + @updates[:spend_time] = { duration: :reset, user: current_user } + end + # This is a dummy command, so that it appears in the autocomplete commands desc 'CC' params '@user' diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 8b48d90f60bda4076a0c03d2aac19a387563a67f..5ca2551ee61680821b317b311fb58164cfb647a4 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -109,6 +109,57 @@ module SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when the estimated time of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # time_estimate - Estimated time + # + # Example Note text: + # + # "Changed estimate of this issue to 3d 5h" + # + # Returns the created Note object + + def change_time_estimate(noteable, project, author) + parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) + body = if noteable.time_estimate == 0 + "Removed time estimate on this #{noteable.human_class_name}" + else + "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" + end + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the spent time of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # time_spent - Spent time + # + # Example Note text: + # + # "Added 2h 30m of time spent on this issue" + # + # Returns the created Note object + + def change_time_spent(noteable, project, author) + time_spent = noteable.time_spent + + if time_spent == :reset + body = "Removed time spent on this #{noteable.human_class_name}" + else + parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) + action = time_spent > 0 ? 'Added' : 'Subtracted' + body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}" + end + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the status of a Noteable is changed # # noteable - Noteable object @@ -146,7 +197,7 @@ module SystemNoteService end def remove_merge_request_wip(noteable, project, author) - body = 'unmarked as a Work In Progress' + body = 'unmarked as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d211d5ebbe9460872c754b575536e7048f6103e --- /dev/null +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -0,0 +1,127 @@ +module Users + # Service for refreshing the authorized projects of a user. + # + # This particular service class can not be used to update data for the same + # user concurrently. Doing so could lead to an incorrect state. To ensure this + # doesn't happen a caller must synchronize access (e.g. using + # `Gitlab::ExclusiveLease`). + # + # Usage: + # + # user = User.find_by(username: 'alice') + # service = Users::RefreshAuthorizedProjectsService.new(some_user) + # service.execute + class RefreshAuthorizedProjectsService + attr_reader :user + + LEASE_TIMEOUT = 1.minute.to_i + + # user - The User for which to refresh the authorized projects. + def initialize(user) + @user = user + + # We need an up to date User object that has access to all relations that + # may have been created earlier. The only way to ensure this is to reload + # the User object. + user.reload + end + + def execute + lease_key = "refresh_authorized_projects:#{user.id}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. If we don't do so we may end up + # not updating the list of authorized projects properly. To prevent + # hammering Redis too much we'll wait for a bit between retries. + sleep(1) + end + + begin + execute_without_lease + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end + end + + # This method returns the updated User object. + def execute_without_lease + current = current_authorizations_per_project + fresh = fresh_access_levels_per_project + + remove = current.each_with_object([]) do |(project_id, row), array| + # rows not in the new list or with a different access level should be + # removed. + if !fresh[project_id] || fresh[project_id] != row.access_level + array << row.project_id + end + end + + add = fresh.each_with_object([]) do |(project_id, level), array| + # rows not in the old list or with a different access level should be + # added. + if !current[project_id] || current[project_id].access_level != level + array << [user.id, project_id, level] + end + end + + update_authorizations(remove, add) + end + + # Updates the list of authorizations for the current user. + # + # remove - The IDs of the authorization rows to remove. + # add - Rows to insert in the form `[user id, project id, access level]` + def update_authorizations(remove = [], add = []) + return if remove.empty? && add.empty? && user.authorized_projects_populated + + User.transaction do + user.remove_project_authorizations(remove) unless remove.empty? + ProjectAuthorization.insert_authorizations(add) unless add.empty? + user.set_authorized_projects_column + end + + # Since we batch insert authorization rows, Rails' associations may get + # out of sync. As such we force a reload of the User object. + user.reload + end + + def fresh_access_levels_per_project + fresh_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row.access_level + end + end + + def current_authorizations_per_project + current_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row + end + end + + def current_authorizations + user.project_authorizations.select(:project_id, :access_level) + end + + def fresh_authorizations + ProjectAuthorization. + unscoped. + select('project_id, MAX(access_level) AS access_level'). + from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}"). + group(:project_id) + end + + private + + # Returns a union query of projects that the user is authorized to access + def project_authorizations_union + relations = [ + user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), + user.groups_projects.select_for_project_authorization, + user.projects.select_for_project_authorization, + user.groups.joins(:shared_projects).select_for_project_authorization + ] + + Gitlab::SQL::Union.new(relations) + end + end +end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index b6c52ddac7a6a5a16a9e7d327e1ffbcae8a1d35b..86f317dcd18f51d691c78e01e48e0614234c91fe 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -1,4 +1,4 @@ -class ArtifactUploader < CarrierWave::Uploader::Base +class ArtifactUploader < GitlabUploader storage :file attr_accessor :build, :field @@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base def exists? file.try(:exists?) end - - def move_to_cache - true - end - - def move_to_store - true - end end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index fb3b5dfecd06a1d69613ea95a536d5c775dad9a7..cfcb877cc3e5b83df013184465d8e3959f0ca5f3 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,4 +1,4 @@ -class AttachmentUploader < CarrierWave::Uploader::Base +class AttachmentUploader < GitlabUploader include UploaderHelper storage :file diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 38683fdf6d7cb47a274e48a205770df7dd481dc1..265cea2d2c62892e69c6d37c0fe4a12b9ba360aa 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,4 +1,4 @@ -class AvatarUploader < CarrierWave::Uploader::Base +class AvatarUploader < GitlabUploader include UploaderHelper storage :file @@ -10,4 +10,15 @@ class AvatarUploader < CarrierWave::Uploader::Base def exists? model.avatar.file && model.avatar.file.exists? end + + # We set move_to_store and move_to_cache to 'false' to prevent stealing + # the avatar file from a project when forking it. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 + def move_to_store + false + end + + def move_to_cache + false + end end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 3ac6030c21c747a2b76b64bf292876bdfce1e820..47bef7cd1e48675b8a8cb7e7114182b569fbefc7 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,4 +1,4 @@ -class FileUploader < CarrierWave::Uploader::Base +class FileUploader < GitlabUploader include UploaderHelper MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb new file mode 100644 index 0000000000000000000000000000000000000000..02d7c601d6c08667b58d453738d477c194bf8a9f --- /dev/null +++ b/app/uploaders/gitlab_uploader.rb @@ -0,0 +1,11 @@ +class GitlabUploader < CarrierWave::Uploader::Base + # Reduce disk IO + def move_to_cache + true + end + + # Reduce disk IO + def move_to_store + true + end +end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 4f356dd663ee303920027b11dce1b4707259918a..faab539b8e0bc5496018177cd7a949077722c2f5 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -1,4 +1,4 @@ -class LfsObjectUploader < CarrierWave::Uploader::Base +class LfsObjectUploader < GitlabUploader storage :file def store_dir @@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base "#{Gitlab.config.lfs.storage_path}/tmp/cache" end - def move_to_cache - true - end - - def move_to_store - true - end - def exists? file.try(:exists?) end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 927c67b65b07522f636be1c854901916ec62c7ab..36279daa743224b1a75e80abdabb6126b3efbf28 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator # without tree as reserved name routing can match 'group/project' as group name, # 'tree' as project name and 'deploy_keys' as route. # - RESERVED = (NamespaceValidator::RESERVED + + RESERVED = (NamespaceValidator::RESERVED - + %w[dashboard help ci admin search notes services assets profile public] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml index 2741eb443578a2aa43ebd1ef8b01ad2a31cf355c..d50b407174561d5e660c22b872934f713cb908f3 100644 --- a/app/views/abuse_report_mailer/notify.html.haml +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -1,7 +1,7 @@ %p - #{link_to @abuse_report.user.name, user_url(@abuse_report.user)} - (@#{@abuse_report.user.username}) was reported for abuse by - #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} + #{link_to @abuse_report.user.name, user_url(@abuse_report.user)} + (@#{@abuse_report.user.username}) was reported for abuse by + #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} (@#{@abuse_report.reporter.username}). %blockquote diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 7accd2529af704279a86cd9aba9e07e24a24d424..558bbe07b1603f52478f9f9ab444a1852e156d3b 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -321,7 +321,7 @@ = f.text_field :recaptcha_site_key, class: 'form-control' .help-block Generate site and private keys at - %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha .form-group = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' @@ -342,7 +342,7 @@ = f.text_field :akismet_api_key, class: 'form-control' .help-block Generate API key at - %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com + %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com %fieldset %legend Abuse reports @@ -420,6 +420,23 @@ = succeed "." do = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + %fieldset + %legend PlantUML + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :plantuml_enabled do + = f.check_box :plantuml_enabled + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .help-block + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. + %fieldset %legend Usage statistics .form-group diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml index 042971e1eed1a4f5bd7d01049cd9a38ea1cc5ecb..82781f6716dfda43a42d7fdf5ffee2e00eef2135 100644 --- a/app/views/admin/applications/_delete_form.html.haml +++ b/app/views/admin/applications/_delete_form.html.haml @@ -1,4 +1,4 @@ - submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag admin_application_path(application) do - %input{:name => "_method", :type => "hidden", :value => "delete"}/ + %input{ :name => "_method", :type => "hidden", :value => "delete" }/ = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index 4aacbb8cd7791c01485068310af6aa145e8c8e28..c689b26d6e6f02409c9c2db3b78af54bd46a2b56 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -18,6 +18,12 @@ Use %code= Doorkeeper.configuration.native_redirect_uri for local tests + + .form-group + = f.label :scopes, class: 'col-sm-2 control-label' + .col-sm-10 + = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes + .form-actions = f.submit 'Submit', class: "btn btn-save wide" = link_to "Cancel", admin_applications_path, class: "btn btn-default" diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index f8cd98f0ec45edd2aa0b93f5f1b8d07b4d93cb78..b3a3b4c1d4568894cb4951a1d804eb9fe910772b 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -15,7 +15,7 @@ %th %tbody.oauth-applications - @applications.each do |application| - %tr{:id => "application_#{application.id}"} + %tr{ :id => "application_#{application.id}" } %td= link_to application.name, admin_application_path(application) %td= application.redirect_uri %td= application.access_tokens.map(&:resource_owner_id).uniq.count diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 3eb9d61972b314a3fea5e12e7f460b69fbadf933..14683cc66e923d3a065611662097d300b01ffc73 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -2,8 +2,7 @@ %h3.page-title Application: #{@application.name} - -.table-holder +.table-holder.oauth-application-show %table.table %tr %td @@ -23,6 +22,9 @@ - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri + + = render "shared/tokens/scopes_list", token: @application + .form-actions = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 05855db963a44004d8fe5cc0e855e15ad772d5b6..4f982a6e3693de0e8a79da1fa7f618435c592af9 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -43,4 +43,4 @@ .panel.panel-default - %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} + %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" } diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index c05538a393cad6011093aad892a82a8d667ed668..4f2ae081d7a6e2a306b83329a55d2b09b759cc29 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -10,7 +10,7 @@ %br.clearfix --if @broadcast_messages.any? +- if @broadcast_messages.any? %table.table %thead %tr diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index ec40391a3e3a83d2afce1940fdbe65e0672bd840..b5f96363230b5729ffcbd40b04321115491a9523 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -8,7 +8,7 @@ %span Overview = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do + = link_to admin_projects_path, title: 'Projects' do %span Projects = nav_link(controller: :users) do diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e51f4ac1d934ac7647db796a92d861298f1eb0ab..5238623e936efdc64db0f7fdc89a11ae028035a4 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -116,7 +116,7 @@ .light-well.well-centered %h4 Projects .data - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do %h1= number_with_delimiter(Project.cached_count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 149593e7f46ba0ef121f13bf89c230b4b3d2ed1e..7b71bb5b287b1a9638734252f36665deb3e2f647 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,27 +1,34 @@ - page_title "Deploy Keys" -.panel.panel-default.prepend-top-default - .panel-heading - Public deploy keys (#{@deploy_keys.count}) - .controls - = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" - - if @deploy_keys.any? - .table-holder - %table.table - %thead.panel-heading + +%h3.page-title.deploy-keys-title + Public deploy keys (#{@deploy_keys.count}) + .pull-right + = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted' + +- if @deploy_keys.any? + .table-holder.deploy-keys-list + %table.table + %thead + %tr + %th.col-sm-2 Title + %th.col-sm-4 Fingerprint + %th.col-sm-2 Write access allowed + %th.col-sm-2 Added at + %th.col-sm-2 + %tbody + - @deploy_keys.each do |deploy_key| %tr - %th Title - %th Fingerprint - %th Added at - %th - %tbody - - @deploy_keys.each do |deploy_key| - %tr - %td - %strong= deploy_key.title - %td - %code.key-fingerprint= deploy_key.fingerprint - %td - %span.cgray - added #{time_ago_with_tooltip(deploy_key.created_at)} - %td - = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" + %td + %strong= deploy_key.title + %td + %code.key-fingerprint= deploy_key.fingerprint + %td + - if deploy_key.can_push? + Yes + - else + No + %td + %span.cgray + added #{time_ago_with_tooltip(deploy_key.created_at)} + %td + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right' diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 5c410a695bf12457b1402ab5f7e1a8ab0a6540c3..a064efc231f14ef697a8f309513485a2a8f48779 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -16,6 +16,14 @@ Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh/README") = f.text_area :key, class: "form-control thin_area", rows: 5 + .form-group + .control-label + .col-sm-10 + = f.label :can_push do + = f.check_box :can_push + %strong Write access allowed + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) .form-actions = f.submit 'Create', class: "btn-create btn" diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 664bb417c6a70ea4e76829fc7bc106d2b7193171..e3a77dfdf10ecf8fca92a42c447f9b9ad6e61968 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -2,9 +2,12 @@ %li.group-row{ class: css_class } .controls - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Edit', admin_group_edit_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.badge + = storage_counter(group.storage_size) + %span = icon('bookmark') = number_with_delimiter(group.projects.count) @@ -13,14 +16,14 @@ = icon('users') = number_with_delimiter(group.users.count) - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) } = visibility_level_icon(group.visibility_level, fw: false) .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do - = group.name + = group.full_name - if group.description.present? .description diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 794f910a61feac37454506141713714c61d18191..07775247cfd00be2608d9e7e0162ac4270351071 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -27,6 +27,8 @@ = sort_title_recently_updated = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do = sort_title_oldest_updated + = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do + = sort_title_largest_group = link_to new_admin_group_path, class: "btn btn-new" do New Group %ul.content-list diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 40871e32913bc5b6aae16147e98339aa6f8273e4..30b3fabdd7efce595849e66f5ef20d844248cf8c 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,8 +1,8 @@ - page_title @group.name, "Groups" %h3.page-title - Group: #{@group.name} + Group: #{@group.full_name} - = link_to edit_admin_group_path(@group), class: "btn pull-right" do + = link_to admin_group_edit_path(@group), class: "btn pull-right" do %i.fa.fa-pencil-square-o Edit %hr @@ -38,6 +38,18 @@ %strong = @group.created_at.to_s(:medium) + %li + %span.light Storage: + %strong= storage_counter(@group.storage_size) + ( + = storage_counter(@group.repository_size) + repositories, + = storage_counter(@group.build_artifacts_size) + build artifacts, + = storage_counter(@group.lfs_objects_size) + LFS + ) + %li %span.light Group Git LFS status: %strong @@ -55,8 +67,8 @@ %li %strong = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) %span.pull-right.light %span.monospace= project.path_with_namespace + ".git" .panel-footer @@ -73,8 +85,8 @@ %li %strong = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) %span.pull-right.light %span.monospace= project.path_with_namespace + ".git" @@ -88,10 +100,10 @@ Read more about project permissions %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 + = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) - %div.prepend-top-10 + .prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr = button_tag 'Add users to group', class: "btn btn-create" diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index c217490963f1215cddf3579526cb6d5abe2c0530..551edf1436133f162ff5a62ce7242efe09bb403a 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -29,7 +29,7 @@ System hook will be triggered on set of events like creating project or adding ssh key. But you can also enable extra triggers like Push events. - %div.prepend-top-default + .prepend-top-default = f.check_box :push_events, class: 'pull-left' .prepend-left-20 = f.label :push_events, class: 'list-label' do @@ -54,7 +54,7 @@ = f.submit "Add System Hook", class: "btn btn-create" %hr --if @hooks.any? +- if @hooks.any? .panel.panel-default .panel-heading System hooks (#{@hooks.count}) @@ -70,4 +70,3 @@ - if hook.send(trigger) %span.label.label-gray= trigger.titleize %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} - diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index be224d6685529df268d7932e1f76b42f2c756d3c..77b174fbb2742d59a7fcd1dbfb229f6e45083811 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,4 +1,4 @@ -%li{id: dom_id(label)} +%li{ id: dom_id(label) } .label-row = render_colored_label(label, tooltip: false) = markdown_field(label, :description) diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 824edd171f3672d3192f5a6b0eb67ffe625b9ef9..0a954c20fcdff8ddacf116776d0d398ab81e5aed 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -8,7 +8,7 @@ %div{ class: container_class } %ul.nav-links.log-tabs - loggers.each do |klass| - %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' .row-content-block diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b37b8d4fee78728d820a7fb0cfc144cbb2016556..2e6f03fcde001219214a48ceb21349c5469536b1 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,7 +7,7 @@ %div{ class: container_class } .top-area .prepend-top-default - = form_tag admin_namespaces_projects_path, method: :get do |f| + = form_tag admin_projects_path, method: :get do |f| .search-holder .search-field-holder = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' @@ -41,19 +41,19 @@ = button_tag "Search", class: "btn btn-primary btn-search" %ul.nav-links - - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } = nav_link(opts) do - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do All = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do Private = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do Internal = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public .nav-controls @@ -69,8 +69,8 @@ .controls - if project.archived %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" .title diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 6c7c3c48604aa7a26baf05d8d9cc0d079ee2bacf..2967da6e692f565022fbd5861236742e9656f8ef 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -65,9 +65,16 @@ = @project.repository.path_to_repo %li - %span.light Size - %strong - = repository_size(@project) + %span.light Storage: + %strong= storage_counter(@project.statistics.storage_size) + ( + = storage_counter(@project.statistics.repository_size) + repository, + = storage_counter(@project.statistics.build_artifacts_size) + build artifacts, + = storage_counter(@project.statistics.lfs_objects_size) + LFS + ) %li %span.light last commit: diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 64893b38c58deef0c7f0f8c28cabc1c346173e2c..975bd950ae149f0a99c2829f2e8bec670c37a838 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -1,4 +1,4 @@ -%tr{id: dom_id(runner)} +%tr{ id: dom_id(runner) } %td - if runner.shared? %span.label.label-success shared diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 73038164056f7be693c666305358da2b631f3f8b..ca503e35623a5f24650160d3c31300cc9854159d 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = ci_status_with_icon(build.status) + = render 'ci/status/badge', status: build.detailed_status(current_user) %td.status - if project diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index ce5e21e54cc01b0fb8571154088b25b8056a0a8f..9984e733956afeaf1a0a62983db62be2fd22422f 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -15,10 +15,8 @@ %ul.nav-links = nav_link(path: 'users#show') do = link_to "Account", admin_user_path(@user) - = nav_link(path: 'users#groups') do - = link_to "Groups", groups_admin_user_path(@user) = nav_link(path: 'users#projects') do - = link_to "Projects", projects_admin_user_path(@user) + = link_to "Groups and projects", projects_admin_user_path(@user) = nav_link(path: 'users#keys') do = link_to "SSH keys", keys_admin_user_path(@user) = nav_link(controller: :identities) do diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 2d9588f9d278b19b5a8b6aff4ca94d256b467a0d..3b5c713ac2d275c21aad0f5e1c6999820631a80a 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -18,7 +18,7 @@ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' - unless user == current_user .dropdown.inline - %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } + %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml deleted file mode 100644 index 8f6d13b881a266931a40b8bf3432c085b84d4aaa..0000000000000000000000000000000000000000 --- a/app/views/admin/users/groups.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- page_title "Groups", @user.name, "Users" -= render 'admin/users/head' - -- group_members = @user.group_members.includes(:source) -- if group_members.any? - .panel.panel-default - .panel-heading Groups: - %ul.well-list - - group_members.each do |group_member| - - group = group_member.group - %li.group_member - %span{class: ("list-item-name" unless group_member.owner?)} - %strong= link_to group.name, admin_group_path(group) - .pull-right - %span.light= group_member.human_access - - unless group_member.owner? - = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-times.fa-inverse -- else - .nothing-here-block This user has no groups. diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index dd6b7303493a26d7dfb4ba175f1f29b0b3bc836f..15eaf1c0e67f16c845e0404646a22a0140a2df9f 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -1,15 +1,21 @@ -- page_title "Projects", @user.name, "Users" +- page_title "Groups and projects", @user.name, "Users" = render 'admin/users/head' - if @user.groups.any? .panel.panel-default .panel-heading Group projects %ul.well-list - - @user.groups.each do |group| - %li + - @user.group_members.includes(:source).each do |group_member| + - group = group_member.group + %li.group_member %strong= link_to group.name, admin_group_path(group) – access to #{pluralize(group.projects.count, 'project')} + .pull-right + %span.light.vertical-align-middle= group_member.human_access + - unless group_member.owner? + = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do + %i.fa.fa-times.fa-inverse .row .col-md-6 @@ -35,8 +41,8 @@ - if member.owner? %span.light Owner - else - %span.light= member.human_access + %span.light.vertical-align-middle= member.human_access - if member.respond_to? :project - = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do + = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do %i.fa.fa-times diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 76c9ed0ee8bb7d838567aa18083862715a5b0f6f..a71240986c96662fea32ef5948ee87a13b06c197 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -40,7 +40,7 @@ %li.two-factor-status %span.light Two-factor Authentication: - %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} + %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' } - if @user.two_factor_enabled? Enabled = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index d8912eda3146af3b212c8940e56f09423bf4d24b..e3305e21e96c8c6aba6d3d5a0a35aad8522a46fd 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -2,8 +2,7 @@ .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", - disabled: !current_user, - class: (award_active_class(awards, current_user)), + class: (award_state_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index 61c7cce20b24c5a9031474a1284e2e6e5a30f4e0..c91602fcff77b10c6e0b6c20be090bdc8aa29db7 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -36,7 +36,7 @@ - if build[:allow_failure] %b Allowed to fail --else +- else %p %b Status: syntax is incorrect diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 889086c62b166e79a83945f5d80bcaa84f948394..95eb9a5715218a10c728649e3dc8814afa829f1c 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,20 +1,25 @@ - page_title "CI Lint" - page_description "Validate your GitLab CI configuration file" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') %h2 Check your .gitlab-ci.yml -%hr -.row - = form_tag ci_lint_path, method: :post do - .form-group - = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap') +.ci-linter + .row + = form_tag ci_lint_path, method: :post do + .form-group + .col-sm-12 + .file-holder + .file-title.clearfix + Content of .gitlab-ci.yml + #ci-editor.ci-editor #{@content} + = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) .col-sm-12 - = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true) - .col-sm-12 - .pull-left.prepend-top-10 - = submit_tag('Validate', class: 'btn btn-success submit-yml') + .pull-left.prepend-top-10 + = submit_tag('Validate', class: 'btn btn-success submit-yml') -.row.prepend-top-20 - .col-sm-12 - .results - = render partial: 'create' if defined?(@status) + .row.prepend-top-20 + .col-sm-12 + .results.ci-template + = render partial: 'create' if defined?(@status) diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..601fb7f0f3f892a44d7bba0b5974db30ba4a9a79 --- /dev/null +++ b/app/views/ci/status/_badge.html.haml @@ -0,0 +1,11 @@ +- status = local_assigns.fetch(:status) +- css_classes = "ci-status ci-#{status.group}" + +- if status.has_details? + = link_to status.details_path, class: css_classes do + = custom_icon(status.icon) + = status.text +- else + %span{ class: css_classes } + = custom_icon(status.icon) + = status.text diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..8dea3479f823106645062264f700a2a470fa459c --- /dev/null +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -0,0 +1,19 @@ +-# Renders the content of each li in the dropdown + +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" + +- if status.has_details? + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do + %span{ class: klass }= custom_icon(status.icon) + %span.ci-build-text= subject.name +- else + .mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } } + %span{ class: klass }= custom_icon(status.icon) + %span.ci-build-text= subject.name + +- if status.has_action? + = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do + = icon(status.action_icon, class: status.action_class) diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..dd2f649de9a3ec25549c87ffd2d7d77e66abed9b --- /dev/null +++ b/app/views/ci/status/_graph_badge.html.haml @@ -0,0 +1,20 @@ +-# Renders the graph node with both the status icon, status name and action icon + +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" + +- if status.has_details? + = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do + %span{ class: klass }= custom_icon(status.icon) + .ci-status-text= subject.name +- else + .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } } + %span{ class: klass }= custom_icon(status.icon) + .ci-status-text= subject.name + +- if status.has_action? + = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do + %i.ci-action-icon-wrapper + = icon(status.action_icon, class: status.action_class) diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index b78e70ebc1e936554adaf03c090b3ddb0fd65e85..02b94beee928a0e2c031f51cd6daff2597aa918d 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,7 +1,7 @@ %ul.nav-links - %li{ class: ("active" unless params[:filter]) } + %li{ class: ("active" unless params[:filter]) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects - %li{ class: ("active" if params[:filter] == 'starred') } + %li{ class: ("active" if params[:filter] == 'starred') }> = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do Starred Projects diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index 4a55aac0df6785016767c8f850e991bd8ef3bb46..1bbd4602ecf0a587a02358e615e39d94b9ccf350 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -33,7 +33,7 @@ = link_to new_project_path, class: "btn btn-new" do New project --if publicish_project_count > 0 +- if publicish_project_count > 0 .blank-state .blank-state-icon = icon("globe") diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index cc077fad32ab805af72a2839258533e9292bfd81..9849b31d7e26ec0bc167b98d61db38af3721bd6f 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,4 +1,4 @@ -%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } +%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } } = author_avatar(todo, size: 40) .todo-item.todo-block diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 62f52086be41ea60027a50633cdd6be005f4b470..f4efcfb27b2d35479dc8a2c2a33eb05252ce4549 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -5,14 +5,14 @@ .top-area %ul.nav-links - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{class: "todos-pending #{todo_pending_active}"} + %li{ class: "todos-pending #{todo_pending_active}" }> = link_to todos_filter_path(state: 'pending') do %span To do %span.badge = number_with_delimiter(todos_pending_count) - todo_done_active = ('active' if params[:state] == 'done') - %li{class: "todos-done #{todo_done_active}"} + %li{ class: "todos-done #{todo_done_active}" }> = link_to todos_filter_path(state: 'done') do %span Done @@ -32,7 +32,7 @@ - if params[:project_id].present? = hidden_field_tag(:project_id, params[:project_id]) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } }) .filter-item.inline - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) @@ -42,15 +42,15 @@ - if params[:type].present? = hidden_field_tag(:type, params[:type]) = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) + data: { data: todo_types_options, default_label: 'Type' } }) .filter-item.inline.actions-filter - if params[:action_id].present? = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) + data: { data: todo_actions_options, default_label: 'Action' } }) .pull-right .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 84e13693dfd18ff58f4ff47da32edfc1e26284d4..5d359538efe85698326fd4b1d3ac11f6073dfdcd 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,16 +1,16 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| - %div.form-group + .form-group = f.label "Username or email", for: :login = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." - %div.form-group + .form-group = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - if devise_mapping.rememberable? .remember-me.checkbox - %label{for: "user_remember_me"} + %label{ for: "user_remember_me" } = f.check_box :remember_me %span Remember me .pull-right.forgot-password = link_to "Forgot your password?", new_password_path(resource_name) - %div.submit-container.move-submit-down + .submit-container.move-submit-down = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index a6cadbcbdff8470a95fb20b355eb6fce02e308bd..2556cb6f59bf5856b19b304888683a6c1b90ff23 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,13 +1,13 @@ = form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do .form-group = label_tag :username, 'Username or email' - = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } + = text_field_tag :username, nil, { class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } .form-group = label_tag :password = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox - %label{for: "remember_me"} + %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me = submit_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 3ab5461f92933087e768d77953b5ec0e216f4e4c..3159d21598ac79627895c1c5dfa10ba1c6da633c 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,13 +1,13 @@ = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do .form-group = label_tag :username, "#{server['label']} Username" - = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } + = text_field_tag :username, nil, { class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } .form-group = label_tag :password = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox - %label{for: "remember_me"} + %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me = submit_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 2cadc424668a022e05099a8962f7968fe934171f..951f03083bfc3abe0930dc003d8cee9cc0c9c83d 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -7,7 +7,7 @@ .login-box .login-body - if @user.two_factor_otp_enabled? - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_u2f_enabled?}" }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) %div diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 8908b64cdac9f7ccc7efae71d8c11008134d5151..e87a16a51575110010fc03278af6ccf80e648aff 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,4 +1,4 @@ -%div.omniauth-container +.omniauth-container %p %span.light Sign in with diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 86edaf14e43287d8f3d54d7233dabc05ccf1672d..eddfce363a72c1737412c786d5b944d47463fa4e 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,18 +1,18 @@ - if form_based_providers.any? - if crowd_enabled? - .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'} + .login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' } .login-body = render 'devise/sessions/new_crowd' - @ldap_servers.each_with_index do |server, i| - .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)} + .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) } .login-body = render 'devise/sessions/new_ldap', server: server - if signin_enabled? - .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'} + .login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' } .login-body = render 'devise/sessions/new_base' - elsif signin_enabled? - .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'} + .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' } .login-body = render 'devise/sessions/new_base' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 3133f6de2e844d208d24b665504f87bfd575004c..01ecf23792578a1ff032e0dfae02debbae38b20b 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,20 +1,23 @@ -#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' } +#register-pane.tab-pane.login-box{ role: 'tabpanel' } .login-body = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = devise_error_messages! - %div.form-group + .form-group = f.label :name = f.text_field :name, class: "form-control top", required: true, title: "This field is required." - %div.username.form-group + .username.form-group = f.label :username = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... - %div.form-group + .form-group = f.label :email = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." + .form-group + = f.label :email_confirmation + = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength = f.label :password = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index aec1b31ce627e737d0511c1027834f9568ea7c96..8c4ad30c832ab80b5509149ee3bb6c11bb6aec79 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -3,7 +3,7 @@ %li.active = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero? && !crowd_enabled?)} + %li{ class: (:active if i.zero? && !crowd_enabled?) } = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' - if signin_enabled? %li diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 05246303fb6ee17d10e604e695e94c3b8829a77f..c225d800a98fe63d15ad911285c9f4beea9d1d6d 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,6 +1,6 @@ -%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} +%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' } %li.active{ role: 'presentation' } - %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in + %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in - if signin_enabled? && signup_enabled? - %li{ role: 'presentation'} - %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register + %li{ role: 'presentation' } + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 1411daeb4a69187730403a621f021bbd00d18541..2deadbeeceb304b55c609c5cdcb28b7f10e0629f 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -1,5 +1,5 @@ - expanded = local_assigns.fetch(:expanded, true) -%tr.notes_holder{class: ('hide' unless expanded)} +%tr.notes_holder{ class: ('hide' unless expanded) } %td.notes_line{ colspan: 2 } %td.notes_content .content diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml index 7ed09dd1a9874b323c79ae655e435a4b51878a30..69bd416c4de8d753f92baa86d806422f5e8b0556 100644 --- a/app/views/discussions/_jump_to_next.html.haml +++ b/app/views/discussions/_jump_to_next.html.haml @@ -5,5 +5,5 @@ %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", title: "Jump to next unresolved discussion", "aria-label" => "Jump to next unresolved discussion", - data: { container: "body" }} + data: { container: "body" } } = custom_icon("next_discussion") diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index f1072ce0febaefdac9514ed35559a2bc27a21512..ef16b516e2c3f823e16f41d37617e188272cd33d 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -1,9 +1,9 @@ - expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?) -%tr.notes_holder{class: ('hide' unless expanded)} +%tr.notes_holder{ class: ('hide' unless expanded) } - if discussion_left %td.notes_line.old %td.notes_content.parallel.old - .content{class: ('hide' unless discussion_left.expanded?)} + .content{ class: ('hide' unless discussion_left.expanded?) } = render "discussions/notes", discussion: discussion_left, line_type: 'old' - else %td.notes_line.old= "" @@ -13,7 +13,7 @@ - if discussion_right %td.notes_line.new %td.notes_content.parallel.new - .content{class: ('hide' unless discussion_right.expanded?)} + .content{ class: ('hide' unless discussion_right.expanded?) } = render "discussions/notes", discussion: discussion_right, line_type: 'new' - else %td.notes_line.new= "" diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml index 001a711b1dd142817fa3d5ea34e19be348ba8203..84b4ce5b606c7d8776336c9c4721114b39299d18 100644 --- a/app/views/doorkeeper/applications/_delete_form.html.haml +++ b/app/views/doorkeeper/applications/_delete_form.html.haml @@ -1,6 +1,6 @@ - submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag oauth_application_path(application) do - %input{:name => "_method", :type => "hidden", :value => "delete"}/ + %input{ :name => "_method", :type => "hidden", :value => "delete" }/ - if defined? small = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do %span.sr-only diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 5c98265727a13e899f315e14d7705efd33e86b7e..b3313c7c985683c58c723303058ca3e4db3bba53 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -17,5 +17,9 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests + .form-group + = f.label :scopes, class: 'label-light' + = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes + .prepend-top-default = f.submit 'Save application', class: "btn btn-create" diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 3998e66f40d5f63bf87b139c10b77ff17457b070..aa271150b07f082d85d0906f3f7d8eeacd6af9f5 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -31,7 +31,7 @@ %th.last-heading %tbody - @applications.each do |application| - %tr{id: "application_#{application.id}"} + %tr{ id: "application_#{application.id}" } %td= link_to application.name, oauth_application_path(application) %td - application.redirect_uri.split.each do |uri| @@ -63,7 +63,7 @@ %tbody - @authorized_apps.each do |app| - token = app.authorized_tokens.order('created_at desc').first - %tr{id: "application_#{app.id}"} + %tr{ id: "application_#{app.id}" } %td= app.name %td= token.created_at %td= token.scopes @@ -72,7 +72,7 @@ %tr %td Anonymous - %div.help-block + .help-block %em Authorization was granted by entering your username and password in the application. %td= token.created_at %td= token.scopes diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 47442b78d48e4972b4f87e59070e83502ac4a7ea..559de63d96dd3c153fe61aa0becf7de33d1411ff 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -2,7 +2,7 @@ %h3.page-title Application: #{@application.name} -.table-holder +.table-holder.oauth-application-show %table.table %tr %td @@ -22,6 +22,9 @@ - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri + + = render "shared/tokens/scopes_list", token: @application + .form-actions = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml index a4c607cea60c50b92b0518f17a21c3df08a707c2..6117b00149f53217922c4a892e22e73853ef874e 100644 --- a/app/views/doorkeeper/authorizations/error.html.haml +++ b/app/views/doorkeeper/authorizations/error.html.haml @@ -1,3 +1,3 @@ %h3.page-title An error has occurred -%main{:role => "main"} +%main{ :role => "main" } %pre= @pre_auth.error_response.body[:error_description] diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index ce050007204f141232e0b81dcd27657e551eaea6..2a0e301c8dd66790a3cfc6edd39b195f6acefa4f 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -1,5 +1,5 @@ %h3.page-title Authorization required -%main{:role => "main"} +%main{ :role => "main" } %p.h4 Authorize %strong.text-info= @pre_auth.client.name diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml index 01f9e46f1428f4326c0c98df9880b234e4f8e435..44e868e678218b45c1a55b383a839eb58bb0d493 100644 --- a/app/views/doorkeeper/authorizations/show.html.haml +++ b/app/views/doorkeeper/authorizations/show.html.haml @@ -1,3 +1,3 @@ %h3.page-title Authorization code: -%main{:role => "main"} +%main{ :role => "main" } %code#authorization_code= params[:code] diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 9f02a8d2ed9d6d7af6ca26666634d21003f7385c..11c1e67878e6523ecd656619ac376ed7c8de807e 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -3,7 +3,7 @@ - path = oauth_authorized_application_path(0, token_id: token) - else - path = oauth_authorized_application_path(application) - + = form_tag path do - %input{:name => "_method", :type => "hidden", :value => "delete"}/ + %input{ :name => "_method", :type => "hidden", :value => "delete" }/ = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm' diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml index b184b9c01d4e8c8c4ab1b07efbcb611c887ba740..c8a585560a2a35ae4550982f770ebbf6a99b135d 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.haml +++ b/app/views/doorkeeper/authorized_applications/index.html.haml @@ -1,6 +1,6 @@ %header.page-header %h1 Your authorized applications -%main{:role => "main"} +%main{ :role => "main" } .table-holder %table.table.table-striped %thead diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index 790d90ad3ee5f5b52de5bf9a7aca479ce9765831..49bd9acd2db156f207e4d239a47fdc5b53521f1f 100644 --- a/app/views/emojis/index.html.haml +++ b/app/views/emojis/index.html.haml @@ -7,5 +7,5 @@ %ul.clearfix.emoji-menu-list - emojis.each do |emoji| %li.pull-left.text-center.emoji-menu-list-item - %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"} + %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" } = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index 8bddbef3562c9fb29dd37bc6bd4794c69121d1cb..a97cbd4d4b38d1280e350a217373b1faa4cd876c 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,6 +1,5 @@ - content_for(:title, 'Access Denied') -%img{:alt => "GitLab Logo", - :src => image_path('logo.svg')} +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } %h1 403 .container diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml index 064ff14ad2c77ef8175ba618aa5f808f522f7fdf..64f7f8e083678a68732091f1f15a226bab1a81ad 100644 --- a/app/views/errors/encoding.html.haml +++ b/app/views/errors/encoding.html.haml @@ -1,6 +1,5 @@ - content_for(:title, 'Encoding Error') -%img{:alt => "GitLab Logo", - :src => image_path('logo.svg')} +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } %h1 500 .container diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml index c5c12a410acf10f878c656f8771f98698c7a9300..d860957665bb90a2ec76ce13366052264df34900 100644 --- a/app/views/errors/git_not_found.html.haml +++ b/app/views/errors/git_not_found.html.haml @@ -1,6 +1,5 @@ - content_for(:title, 'Git Resource Not Found') -%img{:alt => "GitLab Logo", - :src => image_path('logo.svg')} +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } %h1 404 .container diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 50a54a93cb56f5e4a17c18da5e47563b506685ea..a0b9a632e221e21760c5fc355716897c3bc363c9 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,6 +1,5 @@ - content_for(:title, 'Not Found') -%img{:alt => "GitLab Logo", - :src => image_path('logo.svg')} +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } %h1 404 .container diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index d91f1878cb6cebb839b5919f7ac39d2be88ceca7..72508b911341108f165d96c53436a95bbccebd0b 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -1,6 +1,5 @@ - content_for(:title, 'Auth Error') -%img{:alt => "GitLab Logo", - :src => image_path('logo.svg')} +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } %h1 422 .container diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 083c3936212d091f247cb37f7e49ef7fe4e239dd..51585314a629d3d57050e4f66d77c8258231da14 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,2 +1,2 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } = markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index d7e05600627cc65fa7264c52d47b0fededa15a9a..56fc8b86217563be818f350382aea2003d433321 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,2 +1,2 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } = markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index 1154f98282130b7d506c130e370d83d027b7ca29..6fa2f9bd4dbef5086b7c9f185a21e7f9182d7d26 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } = markdown(note.note, pipeline: :atom, project: note.project, author: note.author) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 28bee1d0a33a53a9adf69c801e0b90b4b68d2a29..f8f0bcb76082c754614b66548de67a78b3f2bbf3 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -1,4 +1,4 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } - event.commits.first(15).each do |commit| %p %strong= commit[:author][:name] diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index bba6e0d2c2014d8fe871162c5fcd5799c40cab3c..2fb6b5647dab42df16a2680a1c83c4f9429ae986 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span{class: event.action_name} + %span{ class: event.action_name } - if event.target = event.action_name %strong diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index aba64dd17d0efc9ba4d760f26214fd47e605b6b9..80cf2344fe12fde0cef50711db040d5136b61eec 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span{class: event.action_name} + %span{ class: event.action_name } = event_action_name(event) - if event.project diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml index d8a57560788c95cb3809376f726b85b4bf0509b1..a3b0709e26108f7a64d84db733bc4c396be464db 100644 --- a/app/views/explore/_head.html.haml +++ b/app/views/explore/_head.html.haml @@ -1,5 +1,5 @@ -.explore-title - %h3 +.explore-title.text-center + %h2 Explore GitLab %p.lead Discover projects, groups and snippets. Share your projects with others diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 4e5d965ccbe3244aa6c050aa7a0e8852f84295f2..73cf6e87eb4388e0c9d143a87170c8a69111d8c2 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -17,7 +17,7 @@ .pull-right .dropdown.inline - %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 5ea154c36b46f07868553e4cc6ca9613f9ce6cb9..e30888484922673ebb190e771b7e0f4b52057cbe 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,6 +1,6 @@ - if current_user .dropdown - %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } = icon('globe') %span.light Visibility: - if params[:visibility_level].present? @@ -20,7 +20,7 @@ - if @tags.present? .dropdown - %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } = icon('tags') %span.light Tags: - if params[:tag].present? diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml index af57065f0fc59a2c6366b29759603468e3950dc8..3c622ca5c3c8414f66a8d34492561f7858524d25 100644 --- a/app/views/groups/_group_lfs_settings.html.haml +++ b/app/views/groups/_group_lfs_settings.html.haml @@ -8,4 +8,4 @@ Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/ - %span.descr This setting can be overridden in each project. \ No newline at end of file + %span.descr This setting can be overridden in each project. diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index ebf9aca7700849b90e958f0a851fa37c9f6fcafc..bc5d3c797ac3086c5e76791b64ea79c21ccf1cac 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -21,6 +21,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } = icon("search") + = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading Users with access to diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index d19eaa6add943ba7e0bc06e8bd7c18b35d741f73..38d63fd9acc64be3e3e7d7bb607cfdff2136c54b 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -21,5 +21,5 @@ = render 'shared/group_tips' .form-actions - = f.submit 'Create group', class: "btn btn-create", tabindex: 3 + = f.submit 'Create group', class: "btn btn-create" = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 33fee334d93f805e6f602ccbbe624be77a43b95b..2e7e5e5c30950386fb05194bafa2ba0320991dbd 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -18,8 +18,8 @@ .pull-right - if project.archived %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 52ce26a20b1fbf367df797f5361836b17d0c0ff6..d256d14609e16f461e97bc8a43f8160e2a55094a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -22,7 +22,7 @@ = render 'shared/members/access_request_buttons', source: @group = render 'shared/notifications/button', notification_setting: @notification_setting -%div.groups-header{ class: container_class } +.groups-header{ class: container_class } .top-area %ul.nav-links %li.active @@ -32,6 +32,10 @@ %li = link_to "#shared", 'data-toggle' => 'tab' do Shared Projects + - if @nested_groups.present? + %li + = link_to "#groups", 'data-toggle' => 'tab' do + Subgroups .nav-controls = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false @@ -47,3 +51,8 @@ - if @shared_projects.present? .tab-pane#shared = render "shared_projects", projects: @shared_projects + + - if @nested_groups.present? + .tab-pane#groups + %ul.content-list + = render partial: 'shared/groups/group', collection: @nested_groups diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 65842a0479b8cdb00853d2d7d8d51dff96028086..b74cc8222956576c709a7ebbfda8f8eb3f5419c8 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -1,8 +1,8 @@ -#modal-shortcuts.modal{tabindex: -1} +#modal-shortcuts.modal{ tabindex: -1 } .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h4 Keyboard Shortcuts %small @@ -82,7 +82,7 @@ .col-lg-4 %table.shortcut-mappings - %tbody{ class: 'hidden-shortcut project', style: 'display:none' } + %tbody.hidden-shortcut.project{ style: 'display:none' } %tr %th %th Global Dashboard @@ -190,7 +190,7 @@ %td New issue .col-lg-4 %table.shortcut-mappings - %tbody{ class: 'hidden-shortcut network', style: 'display:none' } + %tbody.hidden-shortcut.network{ style: 'display:none' } %tr %th %th Network Graph @@ -240,7 +240,7 @@ .key shift j %td Scroll to bottom - %tbody{ class: 'hidden-shortcut issues', style: 'display:none' } + %tbody.hidden-shortcut.issues{ style: 'display:none' } %tr %th %th Issues @@ -264,7 +264,7 @@ %td.shortcut .key l %td Change Label - %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } + %tbody.hidden-shortcut.merge_requests{ style: 'display:none' } %tr %th %th Merge Requests diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 070ed90da6dfb0c86bfbd97de8f9bbe160135a6c..dd1df46792bd607cc7f081282fce424caeec7b6a 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -182,7 +182,7 @@ .nav-controls = text_field_tag 'sample', nil, class: 'form-control' .dropdown - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span Sort by name = icon('chevron-down') %ul.dropdown-menu @@ -205,121 +205,121 @@ %h2#buttons Buttons .example - %button.btn.btn-default{:type => "button"} Default - %button.btn.btn-gray{:type => "button"} Gray - %button.btn.btn-primary{:type => "button"} Primary - %button.btn.btn-success{:type => "button"} Success - %button.btn.btn-info{:type => "button"} Info - %button.btn.btn-warning{:type => "button"} Warning - %button.btn.btn-danger{:type => "button"} Danger - %button.btn.btn-link{:type => "button"} Link + %button.btn.btn-default{ :type => "button" } Default + %button.btn.btn-gray{ :type => "button" } Gray + %button.btn.btn-primary{ :type => "button" } Primary + %button.btn.btn-success{ :type => "button" } Success + %button.btn.btn-info{ :type => "button" } Info + %button.btn.btn-warning{ :type => "button" } Warning + %button.btn.btn-danger{ :type => "button" } Danger + %button.btn.btn-link{ :type => "button" } Link %h2#dropdowns Dropdowns .example .clearfix .dropdown.inline.pull-left - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') %ul.dropdown-menu %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .dropdown.inline.pull-right - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-selectable %li - %a.is-active{href: "#"} + %a.is-active{ href: "#" } Dropdown Option .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.is-active{href: "#"} + %a.is-active{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li.divider %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .dropdown-footer %strong Tip: If an author is not a member of this project, you can still filter by his name while using the search field. .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown loading = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading .dropdown-title %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.is-active{href: "#"} + %a.is-active{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li.divider %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .dropdown-footer %strong Tip: @@ -330,21 +330,21 @@ .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: {toggle: 'dropdown' } } Dropdown user = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user .dropdown-title %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.dropdown-menu-user-link.is-active{href: "#"} + %a.dropdown-menu-user-link.is-active{ href: "#" } = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name = @user.name @@ -354,24 +354,24 @@ .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown page 2 = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two .dropdown-page-one .dropdown-title - %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } } = icon('arrow-left') %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.dropdown-menu-user-link.is-active{href: "#"} + %a.dropdown-menu-user-link.is-active{ href: "#" } = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name = @user.name @@ -379,13 +379,13 @@ = @user.to_reference .dropdown-page-two .dropdown-title - %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } } = icon('arrow-left') %span Create label - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Name new label"} + %input.dropdown-input-field{ type: "search", placeholder: "Name new label" } .dropdown-content %button.btn.btn-primary Create @@ -393,16 +393,16 @@ .example %div .dropdown.inline - %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button#js-project-dropdown.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Projects = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Go to project - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content .dropdown-loading @@ -486,22 +486,22 @@ .example %form.form-horizontal .form-group - %label.col-sm-2.control-label{:for => "inputEmail3"} Email + %label.col-sm-2.control-label{ :for => "inputEmail3" } Email .col-sm-10 - %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/ + %input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/ .form-group - %label.col-sm-2.control-label{:for => "inputPassword3"} Password + %label.col-sm-2.control-label{ :for => "inputPassword3" } Password .col-sm-10 - %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/ + %input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/ .form-group .col-sm-offset-2.col-sm-10 .checkbox %label - %input{:type => "checkbox"}/ + %input{ :type => "checkbox" }/ Remember me .form-group .col-sm-offset-2.col-sm-10 - %button.btn.btn-default{:type => "submit"} Sign in + %button.btn.btn-default{ :type => "submit" } Sign in .lead Form when label rendered above input @@ -510,16 +510,16 @@ .example %form .form-group - %label{:for => "exampleInputEmail1"} Email address - %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/ + %label{ :for => "exampleInputEmail1" } Email address + %input#exampleInputEmail1.form-control{ :placeholder => "Enter email", :type => "email" }/ .form-group - %label{:for => "exampleInputPassword1"} Password - %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/ + %label{ :for => "exampleInputPassword1" } Password + %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/ .checkbox %label - %input{:type => "checkbox"}/ + %input{ :type => "checkbox" }/ Remember me - %button.btn.btn-default{:type => "submit"} Sign in + %button.btn.btn-default{ :type => "submit" } Sign in %h2#file File %h4 diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..864c5c0ff9562ee1a10ee1e7a8c4e2235804d15c --- /dev/null +++ b/app/views/import/_githubish_status.html.haml @@ -0,0 +1,61 @@ +- provider = local_assigns.fetch(:provider) +- provider_title = Gitlab::ImportSources.title(provider) + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= "From #{provider_title}" + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } + %td + = provider_project_link(provider, project.import_source) + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{ id: "repo_#{repo.id}" } + %td + = provider_project_link(provider, repo.full_name) + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } } diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index f8b4b107513991c0005964c2f416ab545332b49f..7f1b9ee71410af7727ae637781d2bf34c8065416 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,5 +1,6 @@ -- page_title "Bitbucket import" -- header_title "Projects", root_path +- page_title 'Bitbucket import' +- header_title 'Projects', root_path + %h3.page-title %i.fa.fa-bitbucket Import projects from Bitbucket @@ -10,13 +11,13 @@ %hr %p - if @incompatible_repos.any? - = button_tag class: "btn btn-import btn-success js-import-all" do + = button_tag class: 'btn btn-import btn-success js-import-all' do Import all compatible projects - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') - else - = button_tag class: "btn btn-success js-import-all" do + = button_tag class: 'btn btn-import btn-success js-import-all' do Import all projects - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') .table-responsive %table.table.import-jobs @@ -30,9 +31,9 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -47,31 +48,41 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank" %td.import-target - = import_project_target(repo['owner'], repo['slug']) + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do + = button_tag class: 'btn btn-import js-add-to-import' do Import - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank' %td.import-target %td.import-actions-job-status - = label_tag "Incompatible Project", nil, class: "label label-danger" + = label_tag 'Incompatible Project', nil, class: 'label label-danger' - if @incompatible_repos.any? %p One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git. Please convert - = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview" + = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' and go through the - = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" + = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true' again. .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index c8a6fa1aa9ed76a227b019079f6f9e0a00bba9f2..97e5e51abe0692fbe93a67e10168595e9583b9e9 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -29,7 +29,7 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td = project.import_source %td @@ -46,7 +46,7 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} + %tr{ id: "repo_#{repo.id}" } %td = repo.name %td.import-target diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..02a116f996b52e424199f5c5a3ff5f0586e4bb80 --- /dev/null +++ b/app/views/import/gitea/new.html.haml @@ -0,0 +1,23 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path + +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + +%p + To get started, please enter your Gitea Host URL and a + = succeed '.' do + = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' + += form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do + .form-group + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label' + .col-sm-4 + = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' + .form-group + = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label' + .col-sm-4 + = text_field_tag :personal_access_token, nil, class: 'form-control' + .form-actions + = submit_tag 'List Your Gitea Repositories', class: 'btn btn-create' diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..589ca27e45d671f678a9ca0938a40153c8af3e18 --- /dev/null +++ b/app/views/import/gitea/status.html.haml @@ -0,0 +1,7 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + += render 'import/githubish_status', provider: 'gitea' diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 4c721d40b55e39666884a4912dbb5f4f919a1dcb..0fe578a0036bf66092d95d977f07da94a15c530f 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,64 +1,6 @@ -- page_title "GitHub import" +- page_title "GitHub Import" - header_title "Projects", root_path %h3.page-title - %i.fa.fa-github - Import projects from GitHub + = icon 'github', text: 'Import Projects from GitHub' -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From GitHub - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = github_project_link(project.import_source) - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = github_project_link(repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-btn - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } } += render 'import/githubish_status', provider: 'github' diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index d31fc2e6adb6d2c5b1f35eec687f6c1da63091dd..d5b88709a349c163a616bf77a343f1f688a7491b 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -24,7 +24,7 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td @@ -41,7 +41,7 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo["id"]}"} + %tr{ id: "repo_#{repo["id"]}" } %td = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" %td.import-target diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index 5d2f149cd5f013810b83045674f5cffc4caab95b..336becd229e8d3d6a5efcd0ea7840f6cd7d53c4c 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -45,7 +45,7 @@ %p Upload <code>GoogleCodeProjectHosting.json</code> here: %p - %input{type: "file", name: "dump_file", id: "dump_file"} + %input{ type: "file", name: "dump_file", id: "dump_file" } %li %p Do you want to customize how Google Code email addresses and usernames are imported into GitLab? diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index e79f122940ae6c122b47951a3b5d17f4dd7d0f16..9f1507cade6c6b40d3b19b36d2f40d1025b1d3e7 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -34,7 +34,7 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" %td @@ -51,7 +51,7 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} + %tr{ id: "repo_#{repo.id}" } %td = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" %td.import-target @@ -61,7 +61,7 @@ Import = icon("spinner spin", class: "loading-icon") - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo.id}"} + %tr{ id: "repo_#{repo.id}" } %td = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" %td.import-target diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 125f09777ba50366f6e0499f774e39a584cda27d..c93dc7a50e89ae5a52a79800b8405c8d150a3d70 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -6,8 +6,8 @@ -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.last? - %li{ class: "next disabled" } + %li.next.disabled %span= raw(t 'views.pagination.next') - else - %li{ class: "next" } + %li.next = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 750aed8f329350297d38dd91989b6e2c1a87d322..cefe0066a8f4dd9bc7c9c566a8203767ee7434c9 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"} - = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} +%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" } + = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil } diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index f5e0d2ed3f30dfd54914eafbf51ee7ecc1b6dafc..8fe6bd653ae41dfd54c3ef5082f8646ac8fd7546 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -6,7 +6,7 @@ -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside = paginator.render do - %div.gl-pagination + .gl-pagination %ul.pagination.clearfix - unless current_page.first? = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages @@ -19,4 +19,3 @@ = next_page_tag - unless current_page.last? = last_page_tag unless total_pages < 5 - diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index 7edf10498a80e230d03240f393351f538a3b08cb..b7c6caf7ff43dc8fdfafa24b5086fd9004a31805 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -6,8 +6,8 @@ -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.first? - %li{ class: "prev disabled" } + %li.prev.disabled %span= raw(t 'views.pagination.previous') - else - %li{ class: "prev" } + %li.prev = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3e488cf73b97a65c97c4b675f4138f603b16376b..3096f0ee19ee81dcfca025b00eaaf23b34561241 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,27 +1,27 @@ - page_description brand_title unless page_description - site_name = "GitLab" -%head{prefix: "og: http://ogp.me/ns#"} - %meta{charset: "utf-8"} - %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} +%head{ prefix: "og: http://ogp.me/ns#" } + %meta{ charset: "utf-8" } + %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' } -# Open Graph - http://ogp.me/ - %meta{property: 'og:type', content: "object"} - %meta{property: 'og:site_name', content: site_name} - %meta{property: 'og:title', content: page_title} - %meta{property: 'og:description', content: page_description} - %meta{property: 'og:image', content: page_image} - %meta{property: 'og:url', content: request.base_url + request.fullpath} + %meta{ property: 'og:type', content: "object" } + %meta{ property: 'og:site_name', content: site_name } + %meta{ property: 'og:title', content: page_title } + %meta{ property: 'og:description', content: page_description } + %meta{ property: 'og:image', content: page_image } + %meta{ property: 'og:url', content: request.base_url + request.fullpath } -# Twitter Card - https://dev.twitter.com/cards/types/summary - %meta{property: 'twitter:card', content: "summary"} - %meta{property: 'twitter:title', content: page_title} - %meta{property: 'twitter:description', content: page_description} - %meta{property: 'twitter:image', content: page_image} + %meta{ property: 'twitter:card', content: "summary" } + %meta{ property: 'twitter:title', content: page_title } + %meta{ property: 'twitter:description', content: page_description } + %meta{ property: 'twitter:image', content: page_image } = page_card_meta_tags %title= page_title(site_name) - %meta{name: "description", content: page_description} + %meta{ name: "description", content: page_description } = favicon_link_tag 'favicon.ico' @@ -36,20 +36,20 @@ = csrf_meta_tags - unless browser.safari? - %meta{name: 'referrer', content: 'origin-when-cross-origin'} - %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} - %meta{name: 'theme-color', content: '#474D57'} + %meta{ name: 'referrer', content: 'origin-when-cross-origin' } + %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' } + %meta{ name: 'theme-color', content: '#474D57' } -# Apple Safari/iOS home screen icons = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon' = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76' = favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120' = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152' - %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'} + %link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' } -# Windows 8 pinned site tile - %meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')} - %meta{name: 'msapplication-TileColor', content: '#30353E'} + %meta{ name: 'msapplication-TileImage', content: image_path('msapplication-tile.png') } + %meta{ name: 'msapplication-TileColor', content: '#30353E' } = yield :meta_tags diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index e138ebab0188767b60b2313c3883ea30ee26a4f2..3daa1e90a8c335fd41db5b9042af4a14d6399446 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -3,6 +3,14 @@ - if project :javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" - GitLab.GfmAutoComplete.cachedData = undefined; - GitLab.GfmAutoComplete.setup(); + gl.GfmAutoComplete.dataSources = { + emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}", + members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", + issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", + mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}", + labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}", + milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}", + commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" + }; + + gl.GfmAutoComplete.setup(); diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index a9a0b149049661e5a981759a593177111330144f..54d02ee8e4b961a799122f26dc6063e238b38f5d 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -22,9 +22,10 @@ = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav - = render "layouts/broadcast" - = render "layouts/flash" - = yield :flash_message + .alert-wrapper + = render "layouts/broadcast" + = render "layouts/flash" + = yield :flash_message %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 8e65bd12c569befa909dd63a78270f10b30f6953..0e64ebd71b840872ee26b73bf00166a598cc415a 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,7 +6,7 @@ - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) } - if @project && @project.persisted? - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) } -.search.search-form{class: "#{'has-location-badge' if label.present?}"} +.search.search-form{ class: "#{'has-location-badge' if label.present?}" } = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - if label.present? @@ -44,4 +44,4 @@ = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' - .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } + .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6c2285fa2b6322fe1b5d241521aef3cd757deccb..935517d49135bf981cc81410858d1f76a02a6623 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" - %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}} + %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index afd9958f0738effa3c2a7a90d9609dea5f967f33..3368a9beb29646a20fef41fe158f558cf77599f7 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,7 +1,7 @@ !!! 5 -%html{ lang: "en", class: "devise-layout-html"} +%html.devise-layout-html = render "layouts/head" - %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }} + %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } } .page-wrap = Gon::Base.render_data = render "layouts/header/empty" diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 6bd427b02ac919407a03896dd2924a034444c7be..7466423a934ec6b8420268a4e1d08043cfd56f9a 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: "en"} +%html{ lang: "en" } = render "layouts/head" %body.ui_charcoal.login-page.application.navless = Gon::Base.render_data diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml index c258eafdd51c2234939c69fd7a6ce699d727e074..e1e1f9ae516536762ab23e0f94c6ff758f509466 100644 --- a/app/views/layouts/devise_mailer.html.haml +++ b/app/views/layouts/devise_mailer.html.haml @@ -1,7 +1,7 @@ !!! 5 %html %head - %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type') + %meta{ content: 'text/html; charset=UTF-8', 'http-equiv'=> 'Content-Type' } = stylesheet_link_tag 'mailers/devise' %body @@ -9,7 +9,7 @@ %tr %td %table#header - %td{valign: "top"} + %td{ valign: "top" } = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') %table#body diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index a3b925f6afd6ebf8914c7d3e50e2247583b3258f..6d9ec043590acf0ff362b5e1ec9062c85a59621b 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,7 +1,7 @@ !!! 5 -%html{ lang: "en"} +%html{ lang: "en" } %head - %meta{:content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport"} + %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" } %title= yield(:title) :css body { diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 5456be77aabaae02388126de38fc1448443d27e2..9ecc0d11c9588c61fc4ebfab9d9fe5f32b41fb79 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,11 +1,11 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - %a{ href: "#content-body", tabindex: "1", class: "sr-only gl-accessibility" } Skip to content - %div{ class: "container-fluid" } + %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content + .container-fluid .header-content %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %span.sr-only Toggle navigation = icon('bars') - %button.navbar-toggle{type: 'button'} + %button.navbar-toggle{ type: 'button' } %span.sr-only Toggle navigation = icon('ellipsis-v') @@ -44,7 +44,7 @@ %li = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li - = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } + = link_to "Settings", profile_path, aria: { label: "Settings" } %li = link_to "Help", help_path, aria: { label: "Help" } %li.divider diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml index 38e9b80d129c38b3979a13d33cdc3dffd3ebf2b4..9de0e12a826e9d8fa466966794ca0442a349cf8e 100644 --- a/app/views/layouts/nav/_admin_settings.html.haml +++ b/app/views/layouts/nav/_admin_settings.html.haml @@ -1,6 +1,6 @@ .controls .dropdown.admin-settings-dropdown - %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 817e4bebb058c96abc7b78feaf4e94f427d9145f..205d23178d278234d272cdeedd9c8f40b5b8a644 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -35,3 +35,7 @@ = link_to dashboard_snippets_path, title: 'Snippets' do %span Snippets + + = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do + %span + About GitLab CE diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 1579d8f16625b4a0aaa89e1f969c5c7545f762e3..30feb6813b4a0d998d62ab2136a87fdbb1133f81 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -5,7 +5,7 @@ - if can_admin_group || can_edit .controls .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 904d11c2cf4d40b542c0f8fbd7e7e2843de907a8..a8bbd67de80623d75ee75fe1f3142610a6b71479 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,7 +1,7 @@ - if current_user .controls .dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 613b8b7d3013d1b3c30e5156ef20a0f519e9898f..0fb2bb460cbbd096ff750969b6db6e9d7e02926e 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,14 +1,9 @@ - if project_nav_tab? :team - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do + = nav_link(controller: [:members, :teams]) do + = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span Members - if can_edit - - if @project.allowed_to_share_with_group? - = nav_link(controller: :group_links) do - = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do - %span - Groups = nav_link(controller: :deploy_keys) do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do %span diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 1ec4c3f0c6730222f37263cb1dbc457f8b644e2e..76268c1b7056cc1a21f122fc8814168711bf3713 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -1,14 +1,14 @@ -%html{lang: "en"} +%html{ lang: "en" } %head - %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" } %title GitLab = stylesheet_link_tag 'notify' = yield :head %body - %div.content + .content = yield - %div.footer{style: "margin-top: 10px;"} + .footer{ style: "margin-top: 10px;" } %p — %br diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index b77d3402a2e6381e56e066905c0b5938ed8a4dd6..0ee8a57dbd4d1b4bda4f8837da0a538d81ba5080 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,5 @@ -- page_title "Profile Settings" -- header_title "Profile Settings", profile_path unless header_title +- page_title "User Settings" +- header_title "User Settings", profile_path unless header_title - sidebar "dashboard" - nav "profile" diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index 4bf7c1f4d64b9ef0b02ae485a21cadee81acf741..a744c4be9d6ba0b0f638f6125db3e23d2bfb89a8 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,5 +1,5 @@ - content_for :header do - %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } GitLab (build failed) %h3 diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 252a5b7152c225f69b4fb7f9e5b93d852e2e744a..8c2e6db1426dc8bf4aaa9e0b858777c2efaababa 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,5 +1,5 @@ - content_for :header do - %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } GitLab (build successful) %h3 diff --git a/app/views/notify/links/ci/builds/_build.html.haml b/app/views/notify/links/ci/builds/_build.html.haml index 38cd4e5e1453419a37583277e70445708ef4a4f3..d35b3839171ef8ca5f1f6bfd050f90e72bcac51e 100644 --- a/app/views/notify/links/ci/builds/_build.html.haml +++ b/app/views/notify/links/ci/builds/_build.html.haml @@ -1,2 +1,2 @@ -%a{href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;"} +%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" } = build.name diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index f42b150c0d60cd2014e0dab66d76c1fe34fe96ff..d1855568215dfda6dc3d593c580b479cb71c324f 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,7 +1,7 @@ - if current_application_settings.email_author_in_body %div #{link_to @issue.author_name, user_url(@issue.author)} wrote: --if @issue.description +- if @issue.description = markdown(@issue.description, pipeline: :email, author: @issue.author) - if @issue.assignee_id.present? diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml index 4f3d36bd9cada54e93286fab68177e0953ac7060..02f21baa368ccb883e408abcc1e256a53f2671f7 100644 --- a/app/views/notify/new_mention_in_issue_email.html.haml +++ b/app/views/notify/new_mention_in_issue_email.html.haml @@ -4,7 +4,7 @@ - if current_application_settings.email_author_in_body %div #{link_to @issue.author_name, user_url(@issue.author)} wrote: --if @issue.description +- if @issue.description = markdown(@issue.description, pipeline: :email, author: @issue.author) - if @issue.assignee_id.present? diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml index 32aedb9e6b91a3259be0c1a6af41b370888569ed..cbd434be02a9779bc323a575d9a687de12b36bd3 100644 --- a/app/views/notify/new_mention_in_merge_request_email.html.haml +++ b/app/views/notify/new_mention_in_merge_request_email.html.haml @@ -11,5 +11,5 @@ %p Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} --if @merge_request.description +- if @merge_request.description = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 158404de396084988c8516a7173de9492972ba9e..8890b300f7d0145073cc5b5356065d08490b972a 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -8,5 +8,5 @@ %p Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} --if @merge_request.description +- if @merge_request.description = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 001d9c48555aba9834b3709a4def8b75863dff2a..82c7fe229b805015bed28e7037175c0d7fcac251 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -1,9 +1,9 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{lang: "en"} +%html{ lang: "en" } %head - %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ - %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ - %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/ + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ %title= message.subject :css /* CLIENT-SPECIFIC STYLES */ @@ -41,139 +41,139 @@ padding-right: 10px !important; } } - %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } %tbody %tr.line - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  %tr.header - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } %tbody %tr.alert - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} - %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } Your pipeline has failed. %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } = namespace_name \/ - %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } = @project.name %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } = @pipeline.ref %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = @pipeline.short_sha - if @merge_request in - %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"} + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } = @merge_request.to_reference - .commit{style: "color:#5c5c5c;font-weight:300;"} + .commit{ style: "color:#5c5c5c;font-weight:300;" } = @pipeline.git_commit_message.truncate(50) %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - commit = @pipeline.commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author - %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } = commit.author.name - else %span = commit.author_name %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - failed = @pipeline.statuses.latest.failed %tr.pre-section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } Pipeline - %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = "\##{@pipeline.id}" had = failed.size failed = "#{'build'.pluralize(failed.size)}." %tr.warning - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } Logs may contain sensitive data. Please consider before forwarding this email. %tr.section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"} - %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } + %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } %tbody - failed.each do |build| %tr.build-state - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"} - %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } + %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } = build.stage - %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} + %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %tr.build-log - if build.has_trace? - %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"} - %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"} + %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } + %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } = build.trace_html(last_lines: 10).html_safe - else - %td{colspan: "2"} + %td{ colspan: "2" } %tr.footer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications · - %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help %div You're receiving this email because of your account on = succeed "." do - %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 56c1949ab2b93cc13542069a261a0315fd1deebe..6dddb3b63731d372f1bbfeb48d27ab77cff9353c 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1,9 +1,9 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{lang: "en"} +%html{ lang: "en" } %head - %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ - %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ - %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/ + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ %title= message.subject :css /* CLIENT-SPECIFIC STYLES */ @@ -41,114 +41,114 @@ padding-right: 10px !important; } } - %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } %tbody %tr.line - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  %tr.header - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } %tbody %tr.success - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} - %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } Your pipeline has passed. %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } = namespace_name \/ - %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } = @project.name %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } = @pipeline.ref %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = @pipeline.short_sha - if @merge_request in - %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"} + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } = @merge_request.to_reference - .commit{style: "color:#5c5c5c;font-weight:300;"} + .commit{ style: "color:#5c5c5c;font-weight:300;" } = @pipeline.git_commit_message.truncate(50) %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - commit = @pipeline.commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author - %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } = commit.author.name - else %span = commit.author_name %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.success-message - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } - build_count = @pipeline.statuses.latest.size - stage_count = @pipeline.stages_count Pipeline - %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = "\##{@pipeline.id}" successfully completed = "#{build_count} #{'build'.pluralize(build_count)}" in = "#{stage_count} #{'stage'.pluralize(stage_count)}." %tr.footer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications · - %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help %div You're receiving this email because of your account on = succeed "." do - %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml index b27cb620b9e7d0f88da069943d3fcea6c677a010..27785165c2ddd17ccf8d7cd93937ec3b14807554 100644 --- a/app/views/notify/project_was_not_exported_email.text.haml +++ b/app/views/notify/project_was_not_exported_email.text.haml @@ -3,4 +3,4 @@ = "The errors we encountered were:" - @errors.each do |error| - #{error} \ No newline at end of file + #{error} diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 25883de257c0ac9da0537b7a7cffdd72156fa572..36858fa6f346c44ae8e5bb4638c6ce9f8ad350c4 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -29,7 +29,7 @@ %ul - @message.diffs.each do |diff| %li.file-stats - %a{href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" } + %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" } - if diff.deleted_file %span.deleted-file − @@ -54,8 +54,8 @@ %h4 Changes: - diff_files.each do |diff_file| - file_hash = hexdigest(diff_file.file_path) - %li{id: file_hash} - %a{href: @message.target_url + "##{file_hash}"}< + %li{ id: file_hash } + %a{ href: @message.target_url + "##{file_hash}" }< - if diff_file.deleted_file %strong< = diff_file.old_path diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml index f635acf96e206e956b0cc1e1848c9cadee7715ae..5bf22aa94f1105ccf87360cca16ad7891c35afaf 100644 --- a/app/views/profiles/chat_names/new.html.haml +++ b/app/views/profiles/chat_names/new.html.haml @@ -1,5 +1,5 @@ %h3.page-title Authorization required -%main{:role => "main"} +%main{ :role => "main" } %p.h4 Authorize %strong.text-info= @chat_name_params[:chat_name] diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 3276db6692c54367ca84ab21149923341914d2f6..d2a60ac28678a00476a51d9ca4978e9901be1a98 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -6,6 +6,9 @@ = key.title .description = key.fingerprint + .last-used-at + last used: + = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a' .pull-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index dd7615400dc7dd447fe816a9d1d886bacb8cb8ff..d44603c638c06c31c0c67bf9b73127412986ffea 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -11,6 +11,9 @@ %li %span.light Created on: %strong= @key.created_at.to_s(:medium) + %li + %span.light Last used on: + %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A' .col-md-8 %p diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 931878735014e0e341c9f8448c641d5accd60cd8..71b224a413b0231559bcd63fefb53323493b2993 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -17,5 +17,5 @@ %hr %h5 Your SSH keys (#{@keys.count}) - %div.append-bottom-default + .append-bottom-default = render 'key_table' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index d79a1a9f3682db4462eccbcb826da607165b96c0..5c5e59403658c039de2aa881d5be7e13f9cbefc3 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -3,7 +3,7 @@ %div - if @user.errors.any? - %div.alert.alert-danger + .alert.alert-danger %ul - @user.errors.full_messages.each do |msg| %li= msg diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..3f6efa339537f74bb466e1c343b721aa8c86a345 --- /dev/null +++ b/app/views/profiles/personal_access_tokens/_form.html.haml @@ -0,0 +1,21 @@ +- personal_access_token = local_assigns.fetch(:personal_access_token) +- scopes = local_assigns.fetch(:scopes) + += form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(personal_access_token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: "datepicker form-control" + + .form-group + = f.label :scopes, class: 'label-light' + = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes + + .prepend-top-default + = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 05a2ea67aa2189c8327fe45aa409c21f70ced497..60a561c9f9c1483d48414087780d01d80b9626e3 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -19,7 +19,7 @@ Your New Personal Access Token .form-group = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(clipboard_text: flash[:personal_access_token]) + = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr @@ -28,21 +28,8 @@ Add a Personal Access Token %p.profile-settings-content Pick a name for the application, and we'll give you a unique token. - = form_for [:profile, @personal_access_token], - method: :post, html: { class: 'js-requires-input' } do |f| - = form_errors(@personal_access_token) - - .form-group - = f.label :name, class: 'label-light' - = f.text_field :name, class: "form-control", required: true - - .form-group - = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control", required: false - - .prepend-top-default - = f.submit 'Create Personal Access Token', class: "btn btn-create" + = render "form", personal_access_token: @personal_access_token, scopes: @scopes %hr @@ -56,6 +43,7 @@ %th Name %th Created %th Expires + %th Scopes %th %tbody - @active_personal_access_tokens.each do |token| @@ -67,6 +55,7 @@ = token.expires_at.to_date.to_s(:medium) - else %span.personal-access-tokens-never-expires-label Never + %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." } - else diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 2afa026847a8a45a54bbc40c35d78f49da6c8ec7..feadd863b00dd4f03e9796f2c4c3cf32e995c153 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,7 +1,7 @@ - page_title 'Preferences' = render 'profiles/head' -= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f| += form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 Application theme @@ -10,7 +10,7 @@ .col-lg-9.application-theme - Gitlab::Themes.each do |theme| = label_tag do - .preview{class: theme.css_class} + .preview{ class: theme.css_class } = f.radio_button :theme_id, theme.id = theme.name .col-sm-12 diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 578af9fe98dd6c29db50201338dc34159a52f540..c0c82cde2f6cfe39f0735d504abfc5754645221c 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -18,7 +18,8 @@ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default - = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' %h5.prepend-top-0 Upload new avatar .prepend-top-5.append-bottom-10 @@ -30,7 +31,7 @@ The maximum file size allowed is 200KB. - if @user.avatar? %hr - = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray" + = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray" %hr .row .col-lg-3.profile-settings-sidebar @@ -69,7 +70,7 @@ %span.help-block We also use email for avatar detection if no avatar is uploaded. .form-group = f.label :public_email, class: "label-light" - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2" + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2" %span.help-block This email will be displayed on your public profile. .form-group = f.label :skype, class: "label-light" @@ -101,14 +102,14 @@ .modal-dialog .modal-content .modal-header - %button.close{:type => "button", :'data-dismiss' => "modal"} + %button.close{ :type => "button", :'data-dismiss' => "modal" } %span × %h4.modal-title Position and size your new avatar .modal-body .profile-crop-image-container - %img.modal-profile-crop-image + %img.modal-profile-crop-image{ alt: "Avatar cropper" } .crop-controls .btn-group %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } } @@ -116,5 +117,5 @@ %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } } %span.fa.fa-search-minus .modal-footer - %button.btn.btn-primary.js-upload-user-avatar{:type => "button"} + %button.btn.btn-primary.js-upload-user-avatar{ :type => "button" } Set new profile picture diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 03ac739ade51517de77757c31335e0f6445d2918..558a1d56151421e76806c9cc5bc866c8bcc6bfba 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -30,7 +30,7 @@ To add the entry manually, provide the following details to the application on your phone. %p.prepend-top-0.append-bottom-0 Account: - = current_user.email + = @account_string %p.prepend-top-0.append-bottom-0 Key: = current_user.otp_secret.scan(/.{4}/).join(' ') diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 4f15f2997fbb5be0e98794d94cfdd250036d454d..0ea733cb97852a20d107bc49c7e25a2b9fc95d25 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -9,7 +9,7 @@ = render 'shared/event_filter' - .content_list.project-activity{:"data-href" => activity_project_path(@project)} + .content_list.project-activity{ :"data-href" => activity_project_path(@project) } = spinner :javascript diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index e74fd5b93eaae8f289c2c3dd2ddb5f817b8112a5..c24a496486ce3423c3262712d56984f538782d1e 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -1,8 +1,8 @@ -%div#bitbucket_import_modal.modal +#bitbucket_import_modal.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Import projects from Bitbucket .modal-body To enable importing projects from Bitbucket, @@ -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/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml index d2c1e943db11911cadf1b525e8971e652b4e5273..e2b73cee5a91f84d839d03c4c7bd4f1a44ca7593 100644 --- a/app/views/projects/_customize_workflow.html.haml +++ b/app/views/projects/_customize_workflow.html.haml @@ -1,5 +1,5 @@ .row-content-block.project-home-empty - %div.text-center{ class: container_class } + .text-center{ class: container_class } %h4 Customize your workflow! %p diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml index 08e2fc48be71bec9a8e26da18aefb5cdcc8a1084..dbb3309067032cc4d68a9ac23d4784c276d6ef13 100644 --- a/app/views/projects/_find_file_link.html.haml +++ b/app/views/projects/_find_file_link.html.haml @@ -1,3 +1,3 @@ -= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do - = icon('search') - %span Find File += link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do + = icon('search') + %span Find File diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index e9f39b16aa7b1e0eb346b47e69285182ebe26068..00aef66e1f891a3ee3617d9ea6b8e7784a09d6f3 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -1,8 +1,8 @@ -%div#gitlab_import_modal.modal +#gitlab_import_modal.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Import projects from GitLab.com .modal-body To enable importing projects from GitLab.com, @@ -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/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 5a04c3318cfabb806947cde2a6edbed60ff6bfd5..1b9d87e9969fd402b84c0debc0e58f9f83ded230 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') %h1.project-title = @project.name - %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} + %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false) .project-home-desc @@ -18,11 +18,19 @@ = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - .project-repo-buttons.project-action-buttons + .project-repo-buttons .count-buttons = render 'projects/buttons/star' = render 'projects/buttons/fork' - - if @project.feature_available?(:repository, current_user) - .project-clone-holder - = render "shared/clone_panel" + %span.hidden-xs + - if @project.feature_available?(:repository, current_user) + .project-clone-holder + = render "shared/clone_panel" + + - if current_user && can?(current_user, :download_code, @project) + = render 'projects/buttons/download', project: @project, ref: @ref + = render 'projects/buttons/dropdown' + = render 'shared/notifications/button', notification_setting: @notification_setting + = render 'projects/buttons/koding' + = render 'shared/members/access_request_buttons', source: @project diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 7f530708947084bf3851be26809d252c9548c102..e1fea8ccf3d7cd3e1f5c54523d76aeb7ebafb39a 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,7 +1,8 @@ + - ref = local_assigns.fetch(:ref) - status = commit.status(ref) - if status - = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do + = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do = ci_icon_for_status(status) = ci_label_for_status(status) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 58d961d93ca0b79af8b4136b73a2820ca35e41a3..085f79de7850f4101b45b8289847c6f4f1ec02de 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -15,23 +15,23 @@ %li.pull-right .toolbar-group - = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) - = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) - = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) - = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) - = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) - = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) - = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) + = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) + = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) + = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) + = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) + = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) + = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) + = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) .toolbar-group %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } - =icon("arrows-alt fw") + = icon("arrows-alt fw") .md-write-holder = yield - .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))} + .md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) } - if defined?(referenced_users) && referenced_users - %div.referenced-users.hide + .referenced-users.hide %span = icon("exclamation-triangle") You are about to add diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index f00422dd7c08846400f874156de7bc9c5765045b..41d42740f61a5f9fa5463187b871dfacc630d448 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -7,7 +7,7 @@ - else - can_create_wiki = can?(current_user, :create_wiki, @project) .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } - %div.text-center{ class: container_class } + .text-center{ class: container_class } %h4 This project does not have a wiki homepage yet - if can_create_wiki diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml index def493c56f5148000522770ece1ced9dcf76368f..9e49c93388a55929ab40164e6e9986f9f94139b9 100644 --- a/app/views/projects/artifacts/_tree_directory.html.haml +++ b/app/views/projects/artifacts/_tree_directory.html.haml @@ -1,6 +1,6 @@ - path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path) -%tr.tree-item{ 'data-link' => path_to_directory} +%tr.tree-item{ 'data-link' => path_to_directory } %td.tree-item-file-name = tree_icon('folder', '755', directory.name) %span.str-truncated diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index ede01dcc1aa1843e83008c49928fecd77e55ca2f..d0ff14e45e68b279426639f32cd765d91e07ede9 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -8,7 +8,7 @@ Download artifacts archive .tree-holder - %div.tree-content-holder + .tree-content-holder %table.table.tree-table %thead %tr diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index f63802ac88b023b2c587412e23199eec385d081d..23f54553014e5be5af47bd40d54cf70cb311f51c 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -36,7 +36,7 @@ %td.line-numbers - line_count = blame_group[:lines].count - (current_line...(current_line + line_count)).each do |i| - %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + %a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i } = icon("link") = i \ diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 149ee7c59d6a4b3625408c3d9111d8c173b84f15..f75f438ee4f53c6912340d6b996d5052282a7c00 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -18,11 +18,11 @@ - else = link_to title, '#' -%ul.blob-commit-info.hidden-xs +%ul.blob-commit-info.table-list.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project, ref: @ref -%div#blob-content-holder.blob-content-holder +#blob-content-holder.blob-content-holder %article.file-holder .file-title = blob_icon blob.mode, blob.name diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 4a6aa92e3f3db04685091e0882c416a59e2a3ac2..1d058daa0943301b6766f53a45f3b90d4a5c994f 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -21,6 +21,8 @@ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + .dockerfile-selector.js-dockerfile-selector-wrap.hidden + = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) = button_tag class: 'soft-wrap-toggle btn', type: 'button' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index 4c356d1f07f26be1093662ce622b45f6505bdb19..a486b2fe4913025dbf5744a50cb3525e71a6ce23 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -5,11 +5,11 @@ - # be wrong/strange if RawController modified the data. - blob.load_all_data!(@repository) - blob = sanitize_svg(blob) - %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} + %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" } - else .nothing-here-block The SVG could not be displayed as it is too large, you can #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')} instead. - else - %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))} + %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" } diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 84694203d7d4ca8f013fee573896338193cc324f..7f470b890ba2c1464c6a125859ccbcd40bc8d4ca 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Create New Directory .modal-body = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index 2e1f32fd15e12b6abe3a003b890432c2d98755ea..db6662a95acb953f849467aa5cadde5f9043bcfc 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Delete #{@blob.name} .modal-body diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 57a27ec904e4148b9e8159088340990af9ae7e35..61a7ffdd0abb08c20c0e4b4e48abd92888a37a07 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title #{title} .modal-body = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do @@ -12,7 +12,7 @@ Attach a file by drag & drop or = link_to 'click to upload', '#', class: "markdown-selector" %br - .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} + .dropzone-alerts.alert.alert-danger.data{ style: "display:none" } = render 'shared/new_commit_form', placeholder: placeholder diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index a79ae53c7803e1a39e65a0657298bf49d5b38dfb..538f8591f134d098c91b06569d71efe30adfe1fc 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -13,15 +13,15 @@ - case diff_view - when :inline %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{href: "##{line_old}", data: { linenumber: line_old }} + %a{ href: "##{line_old}", data: { linenumber: line_old } } %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{href: "##{line_new}", data: { linenumber: line_new }} + %a{ href: "##{line_new}", data: { linenumber: line_new } } = line_content - when :parallel - %td.old_line.diff-line-num{data: { linenumber: line_old }} + %td.old_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_old), "##{line_old}" = line_content - %td.new_line.diff-line-num{data: { linenumber: line_new }} + %td.new_line.diff-line-num{ data: { linenumber: line_new } } = link_to raw(line_new), "##{line_new}" = line_content diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index 541dc96c45f949ac167be508f98f65c2a6d49f06..5cafb644b406e956b1f2fb4a026e0aded477350d 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -20,6 +20,6 @@ - else %td.old_line.diff-line-num %td.new_line.diff-line-num - %td.line_content{class: "#{line.type}"}= diff_line_content(line.text) + %td.line_content{ class: "#{line.type}" }= diff_line_content(line.text) - else .nothing-here-block No changes. diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 0ab78a39cf9a98f85370a7d71b53fde427fe4dbb..b6738c3380f791ec826dc94bf6d044d8eee5afd5 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } = render 'projects/last_push' - %div#tree-holder.tree-holder + #tree-holder.tree-holder = render 'blob', blob: @blob - if can_edit_blob?(@blob) diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index 1f31496e73fd7bba9bccbea3583c04b8bcac3870..e4c2aff46ecb34ee72f10713fd99069057ff6a2b 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -17,7 +17,7 @@ ":title" => '"Assigned to " + issue.assignee.name', "v-if" => "issue.assignee", data: { container: 'body' } } - %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 } + %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" } %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", type: "button", "v-if" => "(!list.label || label.id !== list.label.id)", diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index 2125c3387c42f08222cddeee2beff6dad84b1f4b..df7fa9ddaf22d81514e80eefcdf975ca36c54b7f 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -1,23 +1,24 @@ %board-sidebar{ "inline-template" => true, ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" } - %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } - .issuable-sidebar - .block.issuable-sidebar-header - %span.issuable-header-text.hide-collapsed.pull-left - %strong - {{ issue.title }} - %br/ - %span - = precede "#" do - {{ issue.id }} - %a.gutter-toggle.pull-right{ role: "button", - href: "#", - "@click.prevent" => "closeSidebar", - "aria-label" => "Toggle sidebar" } - = custom_icon("icon_close", size: 15) - .js-issuable-update - = render "projects/boards/components/sidebar/assignee" - = render "projects/boards/components/sidebar/milestone" - = render "projects/boards/components/sidebar/due_date" - = render "projects/boards/components/sidebar/labels" - = render "projects/boards/components/sidebar/notifications" + %transition{ name: "boards-sidebar-slide" } + %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } + .issuable-sidebar + .block.issuable-sidebar-header + %span.issuable-header-text.hide-collapsed.pull-left + %strong + {{ issue.title }} + %br/ + %span + = precede "#" do + {{ issue.id }} + %a.gutter-toggle.pull-right{ role: "button", + href: "#", + "@click.prevent" => "closeSidebar", + "aria-label" => "Toggle sidebar" } + = custom_icon("icon_close", size: 15) + .js-issuable-update + = render "projects/boards/components/sidebar/assignee" + = render "projects/boards/components/sidebar/milestone" + = render "projects/boards/components/sidebar/due_date" + = render "projects/boards/components/sidebar/labels" + = render "projects/boards/components/sidebar/notifications" diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml index 8fe1b832071e04ab3d4150bd415f7962b71a3b66..e75ce305440c19776b8ecd2db676294308e14d4f 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -14,7 +14,7 @@ %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username", "v-if" => "issue.assignee" } %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar", - width: "32" } + width: "32", alt: "Avatar" } %span.author {{ issue.assignee.name }} %span.username diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9135cee8364e9c2f40e79f200ee6b426f8a7c6f9..04efc2e996c2aa4c33a9498cf5b36dbd6d9f48fc 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -4,7 +4,7 @@ - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) -%li(class="js-branch-#{branch.name}") +%li{ class: "js-branch-#{branch.name}" } %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do = branch.name @@ -12,12 +12,11 @@ - if branch.name == @repository.root_ref %span.label.label-primary default - elsif @repository.merged_to_root_ref? branch.name - %span.label.label-info.has-tooltip(title="Merged into #{@repository.root_ref}") + %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } merged - if @project.protected_branch? branch.name %span.label.label-success - %i.fa.fa-lock protected .controls.hidden-xs - if merge_project && create_mr_button?(@repository.root_ref, branch.name) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 5fd664c7a93c2d029c136cb2742803e521f31b86..5f8f56150f9dd38bd30d226564659cde6f23ff6c 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,16 +3,17 @@ = render "projects/commits/head" %div{ class: container_class } - .top-area + .top-area.adjust .nav-text - Protected branches can be managed in project settings + Protected branches can be managed in + = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project) .nav-controls = form_tag(filter_branches_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline - %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light = projects_sort_options_hash[@sort] = icon('chevron-down') diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 5a6c8c243fa77a36134a46eb145cebe9c60e37d0..e63bdb38bd873998d5880390ce004a5b87f1ca3d 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -2,7 +2,7 @@ - if @error .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + %button.close{ type: "button", "data-dismiss" => "alert" } × = @error %h3.page-title New Branch diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index f6aa20c4579cb154b87ac4573d8d3ade684eff21..b15be0d861d562dec758bfad92e025e7596f0b26 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,13 +1,13 @@ .content-block.build-header .header-content - = ci_status_with_icon(@build.status) + = render 'ci/status/badge', status: @build.detailed_status(current_user) Build %strong ##{@build.id} in pipeline = link_to pipeline_path(@build.pipeline) do %strong ##{@build.pipeline.id} for commit - = link_to ci_status_path(@build.pipeline) do + = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do %strong= @build.pipeline.short_sha from = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index ce8b66b194518a32aebf7df48f163835c5228fcd..37bf085130a41d8eb1aa071eb8db7967e90ca5ac 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -22,14 +22,14 @@ %p.build-detail-row The artifacts were removed #{time_ago_with_tooltip(@build.artifacts_expire_at)} - - elsif @build.artifacts_expire_at + - elsif @build.has_expiring_artifacts? %p.build-detail-row The artifacts will be removed in %span.js-artifacts-remove= @build.artifacts_expire_at - if @build.artifacts? .btn-group.btn-group-justified{ role: :group } - - if @build.artifacts_expire_at + - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do Keep @@ -114,7 +114,7 @@ - if @build.pipeline.stages_count > 1 .dropdown.build-dropdown .title Stage - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.stage-selection More = icon('chevron-down') %ul.dropdown-menu @@ -125,10 +125,10 @@ .builds-container - HasStatus::ORDERED_STATUSES.each do |build_status| - builds.select{|build| build.status == build_status}.each do |build| - .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}} + .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } = link_to namespace_project_build_path(@project.namespace, @project, build) do = icon('arrow-right') - %span{class: "ci-status-icon-#{build.status}"} + %span{ class: "ci-status-icon-#{build.status}" } = ci_icon_for_status(build.status) %span - if build.name @@ -136,4 +136,4 @@ - else = build.id - if build.retried? - %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'} + %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' } diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 06070f12bbd13dd76dae17d3cc96eea8ff7937f5..c623e39b21f7feb8138b826c2379694d7ea4cd8e 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -19,5 +19,5 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - %div.content-list.builds-content-list + .content-list.builds-content-list = render "table", builds: @builds, project: @project diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index cdeb81372ee1b1684fc3767f9ab51c0877638810..54724ef5cabd2d843644320ccc5a1d885aca137d 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -56,17 +56,22 @@ - else #js-build-scroll.scroll-controls .scroll-step - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down + %a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' } + = custom_icon('scroll_up') + = custom_icon('scroll_up_hover_active') + %a.scroll-link.scroll-bottom{ href: '#down-build-trace', id: 'scroll-bottom', title: 'Scroll to bottom' } + = custom_icon('scroll_down') + = custom_icon('scroll_down_hover_active') - if @build.active? .autoscroll-container - %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} - Enable autoscroll + %span.status-message#autoscroll-status{ data: { state: 'disabled' } } + %span.status-text Autoscroll active + %i.status-icon + = custom_icon('scroll_down_hover_active') + #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output - = icon("refresh spin", class: "js-build-refresh") + .build-loader-animation.js-build-refresh #down-build-trace diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 40bfa01a45a9dec53d5fd1261aa60df6bd65ec42..324a7f8cd3fdc7f74661bb5fe316a951e3e7dbe4 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,5 +1,5 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - .dropdown.inline.download-button + .project-action-button.dropdown.inline %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') = icon("caret-down") diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index d3ccebbe2905468c0a83577197229b6694df9284..67de8699b2eae402b2d5192a791e734007b54b0b 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user - .dropdown.inline - %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} + .project-action-button.dropdown.inline + %a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 27da86b9efe614dc81e70b0b9a82c932a05ee092..851fe44a86d19a3c7b7dbc02a853a549f986d329 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -8,7 +8,7 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do = custom_icon('icon_fork') %span Fork - %div.count-with-arrow + .count-with-arrow %span.arrow = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do = @project.forks_count diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml index fdc80d44253bd049a71cddce5c432b082d7c5778..5d9a776da8931dd9379b0ac136de22523e115605 100644 --- a/app/views/projects/buttons/_koding.html.haml +++ b/app/views/projects/buttons/_koding.html.haml @@ -1,7 +1,3 @@ -- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch) - - if @repository.koding_yml - = link_to koding_project_url(@project), class: 'btn', target: '_blank' do - Run in IDE (Koding) - - else - = link_to add_koding_stack_path(@project), class: 'btn' do - Set Up Koding +- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch) + = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do + Run in IDE (Koding) diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 12d351017705883037bd44298d53a7e3db6b722a..d57eb2cbfbc64e4ad9336318dfa4302f39d8edd2 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -6,7 +6,7 @@ - else = icon('star-o') %span Star - %div.count-with-arrow + .count-with-arrow %span.arrow %span.count.star-count = @project.star_count @@ -15,7 +15,7 @@ = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do = icon('star') Star - %div.count-with-arrow + .count-with-arrow %span.arrow %span.count = @project.star_count diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 18b3b04154f4936ec7c99350572a775fb19d4a6d..520113639b7e20e2c1aaee3f9d362fd12e380385 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -7,12 +7,9 @@ - coverage = local_assigns.fetch(:coverage, false) - allow_retry = local_assigns.fetch(:allow_retry, false) -%tr.build.commit{class: ('retried' if retried)} +%tr.build.commit{ class: ('retried' if retried) } %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) + = render "ci/status/badge", status: build.detailed_status(current_user) %td.branch-commit - if can?(current_user, :read_build, build) diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml deleted file mode 100644 index ad1a7360a8b3e827318de965eeee0bb337a81431..0000000000000000000000000000000000000000 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- is_playable = subject.playable? && can?(current_user, :update_build, @project) -- if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do - = ci_icon_for_status('play') - .ci-status-text= subject.name -- elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - .ci-status-text= subject.name -- else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b58dceb58c9cdb3edae8c67599bb96144fe66d14..990bfbcf951f6d24796c4c9f7960fc64d50ec398 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,13 +1,10 @@ - status = pipeline.status -- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do - = ci_icon_for_status(detailed_status) - = ci_text_for_status(detailed_status) + = render 'ci/status/badge', status: pipeline.detailed_status(current_user) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do @@ -46,10 +43,22 @@ %td.stage-cell - pipeline.stages.each do |stage| - if stage.status - - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" - .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do - = ci_icon_for_status(stage.status) + - detailed_status = stage.detailed_status(current_user) + - icon_status = "#{detailed_status.icon}_borderless" + - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" + + .stage-container.dropdown.js-mini-pipeline-graph + %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } + = custom_icon(icon_status) + = icon('caret-down') + + %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container + .arrow-up + .js-builds-dropdown-list.scrollable-menu + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin + %td - if pipeline.duration @@ -69,18 +78,18 @@ .btn-group.inline - if actions.any? .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') - = icon('caret-down') + = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do = custom_icon('icon_play') - %span= build.name.humanize + %span= build.name - if artifacts.present? .btn-group - %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' } = icon("download") = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml deleted file mode 100644 index b7087749428bec7659c5e5e6ecf09cef13ad5e94..0000000000000000000000000000000000000000 --- a/app/views/projects/commit/_builds.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- @ci_pipelines.each do |pipeline| - = render "pipeline", pipeline: pipeline, pipeline_details: true diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 782f558e8b081c59fd6b43787d6c63f54a92d026..990908211debc2314cb1aa91062680b78711bb77 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -6,19 +6,19 @@ - label = 'Cherry-pick' - target_label = 'Pick into branch' -.modal{id: "modal-#{type}-commit"} +.modal{ id: "modal-#{type}-commit" } .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do + = form_tag [type.underscore, @project.namespace, @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }}) + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) .js-create-merge-request-container diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index cbfd99ca4482ca72b3d1f737512950d3b99e080b..13ab2253733e402cb01084d3005fb50c3b1347ba 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -8,7 +8,3 @@ = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Pipelines %span.badge= @ci_pipelines.count - = nav_link(path: 'commit#builds') do - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do - Builds - %span.badge= @statuses.count diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index c08ed8f6c1687fc2c244ae0f794b3ca6780076ba..08eb0c57f669b87f355d12f3bade600083b21fc6 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,7 +1,7 @@ .page-content-header .header-main-content %strong - = clipboard_button(clipboard_text: @commit.id) + = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") = @commit.short_id %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} @@ -63,7 +63,7 @@ - if @commit.status .well-segment.pipeline-info - %div{class: "icon-container ci-status-icon-#{@commit.status}"} + %div{ class: "icon-container ci-status-icon-#{@commit.status}" } = ci_icon_for_status(@commit.status) Pipeline = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 7f42fde0fea5068a55dca7ea17f220015340cb87..1164627fa11cf73382d3074bd517cd87f5384345 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,15 +1,15 @@ -%ul.content-list.pipelines +%div - if pipelines.blank? - %li + %div .nothing-here-block No pipelines to show - else - .table-holder - %table.table.ci-table - %tbody - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th + .table-holder.pipelines + %table.table.ci-table.js-pipeline-table + %thead + %th.pipeline-status Status + %th.pipeline-info Pipeline + %th.pipeline-commit Commit + %th.pipeline-stages Stages + %th.pipeline-date + %th.pipeline-actions = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml deleted file mode 100644 index 077b2d2725b2c2c4c098fe5ad4f1ca4bff4254a1..0000000000000000000000000000000000000000 --- a/app/views/projects/commit/builds.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- @no_container = true -- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/head" - -%div{ class: container_class } - = render "commit_box" - - = render "ci_menu" - = render "builds" diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index b8c64d1f13e574d02bd1d85f0ff8075603cfadfa..7afd3d80ef526a4f244959ef22def9b880305a11 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -8,7 +8,7 @@ - if @commit.status = render "ci_menu" - else - %div.block-connector + .block-connector = render "projects/diffs/diffs", diffs: @diffs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index a940515fadfd28e1f18f6f2b77a92b037c6ce65b..002e3d345dcf150857debb6030299e6b9a70ad18 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,33 +9,33 @@ - cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do - %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } - = author_avatar(commit, size: 36) + %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" } - .commit-info-block - .commit-row-title - %span.item-title - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - %span.commit-row-message.visible-xs-inline - · - = commit.short_id - - if commit.status(ref) - .visible-xs-inline - = render_commit_status(commit, ref: ref) - - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + .table-list-cell.avatar-cell.hidden-xs + = author_avatar(commit, size: 36) - .commit-actions.hidden-xs - - if commit.status(ref) - = render_commit_status(commit, ref: ref) - = 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) + .table-list-cell.commit-content + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status(ref) + .visible-xs-inline + = render_commit_status(commit, ref: ref) + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - if commit.description? %pre.commit-row-description.js-toggle-content = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) + .commiter + = commit_author_link(commit, avatar: false, size: 24) + committed + #{time_ago_with_tooltip(commit.committed_date)} - = commit_author_link(commit, avatar: false, size: 24) - committed - #{time_ago_with_tooltip(commit.committed_date)} + .table-list-cell.commit-actions.hidden-xs + - if commit.status(ref) + = render_commit_status(commit, ref: ref) + = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") + = 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/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index ce416caa4946a2172cfb02bc8d565eb071f064e4..64d93e4141c465ee20369585170605e4e6b9ec14 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,7 +1,7 @@ - commits, hidden = limited_commits(@commits) - commits = Commit.decorate(commits, @project) -%div.panel.panel-default +.panel.panel-default .panel-heading Commits (#{@commits.count}) - if hidden > 0 @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list= render commits, project: @project, ref: @ref + %ul.content-list.table-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 48756c68941a24b787dc194375b61379a1e7eaaf..fcc367951ad30a53b42841fac20db06462aa40f6 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -4,7 +4,7 @@ - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" %li.commits-row - %ul.list-unstyled.commit-list + %ul.content-list.commit-list.table-list.table-wide = render commits, project: project, ref: ref - if hidden > 0 diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 9628cbd163409b4d4026f657077fbb6f99a9f545..e77f23c7fd878f205463950e2a1b2c51b70fdfbb 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -28,12 +28,12 @@ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - if current_user && current_user.private_token .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do = icon("rss") %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs - %div{id: dom_id(@project)} + %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list = render 'commits', project: @project, ref: @ref = spinner diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 7bde20c32868b2401c9d39eb1a4cdb6050c5f47e..d76d48187cde036cdbabed1a74eb433ee65ebf18 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -2,21 +2,21 @@ .clearfix - if params[:to] && params[:from] .compare-switch-container - = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} + = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'} .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown .input-group.inline-input-group %span.input-group-addon from = hidden_field_tag :from, params[:from] - = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text= params[:from] || 'Select branch/tag' + = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' = render "ref_dropdown" .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to = hidden_field_tag :to, params[:to] - = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do - .dropdown-toggle-text= params[:to] || 'Select branch/tag' + = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do + .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag' = render "ref_dropdown" = button_tag "Compare", class: "btn btn-create commits-compare-btn" diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml index b200ce22970389db7c5cc48b969adf8686abdf07..c3f95860e92375b46db2a01347c642a36869df2d 100644 --- a/app/views/projects/cycle_analytics/_empty_stage.html.haml +++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml @@ -2,6 +2,6 @@ .empty-stage .icon-no-data = custom_icon ('icon_no_data') - %h4 We don’t have enough data to show this stage. + %h4 We don't have enough data to show this stage. %p {{currentStage.emptyStageText}} diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index ef1b38d5e210396e15b15387453744bc53468201..479ce44f378201ef72c330ab7a46f5a3d5d3d75a 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -7,7 +7,7 @@ #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data - .bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"} + .bordered-box.landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()") .row .col-sm-3.col-xs-12.svg-container @@ -20,19 +20,19 @@ = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = icon("spinner spin", "v-show" => "isLoading") - .wrapper{"v-show" => "!isLoading && !hasError"} + .wrapper{ "v-show" => "!isLoading && !hasError" } .panel.panel-default .panel-heading Pipeline Health .content-block .container-fluid .row - .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"} - %h3.header {{item.value}} - %p.text {{item.title}} + .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" } + %h3.header {{ item.value }} + %p.text {{ item.title }} .col-sm-3.col-xs-12.column .dropdown.inline.js-ca-dropdown - %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} + %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } %span.dropdown-label Last 30 days %i.fa.fa-chevron-down %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index 450aaeb367c1b35ee9db78e929171f13bcd189ed..d1e3cb1402269d3a57a65bf90294ee92d66daf39 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -6,6 +6,9 @@ = deploy_key.title .description = deploy_key.fingerprint + - if deploy_key.can_push? + .write-access-allowed + Write access allowed .deploy-key-content.prepend-left-default.deploy-key-projects - deploy_key.projects.each do |project| - if can?(current_user, :read_project, project) diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 901605f7ca3bc05c1f2d757c40ab926f9dafa905..c91bb9c255af73aa71f5ce2cf939b25bae19c500 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -10,4 +10,13 @@ %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") + .form-group + .checkbox + = f.label :can_push do + = f.check_box :can_push + %strong Write access allowed + .form-group + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) + = f.submit "Add key", class: "btn-create btn" diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 58a214bdbd15a74d55729e6a42c5bc0a13110b93..a680b1ca0175aca3e41f1b99cbbe71d583a919b3 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -3,7 +3,7 @@ - if actions.present? .inline .dropdown - %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index ff250eeca50711175b0009e3683423fcc6e70cac..170d786ecbf1c25b9491e87a9067f61cbe38323b 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -1,4 +1,4 @@ -%div.branch-commit +.branch-commit - if deployment.ref .icon-container = deployment.tag? ? icon('tag') : icon('code-fork') diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index 6120b2191dd8efc4f561014c7d9246dc3ab273ed..52a1ece7d605a72ca3dc8a69ed6a0eb502cc5562 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -10,7 +10,7 @@ .nothing-here-block This diff was suppressed by a .gitattributes entry. - elsif diff_file.collapsed? - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) - .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } + .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } This diff is collapsed. %a.click-to-expand Click to expand it. diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index ab4a2dc36e51d604f112862b3db90f841b2acc7a..58c20e225c6c6ba57811c3a060631f275743a239 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -18,8 +18,8 @@ = parallel_diff_btn = render 'projects/diffs/stats', diff_files: diff_files -- if diff_files.overflow? - = render 'projects/diffs/warning', diff_files: diff_files +- if render_overflow_warning?(diff_files) + = render 'projects/diffs/warning', diff_files: diffs .files{ data: { can_create_note: can_create_note } } - diff_files.each_with_index do |diff_file| diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 6c33d80becd972938214e5d848a5cefb2018b938..c37a33bbcd5af4049643c0d9174f37c02d10fab7 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,5 +1,5 @@ -.diff-file.file-holder{id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)} - .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} +.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) } + .file-title = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" - unless diff_file.submodule? diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index d3ed8e1bf3897cb701f575d0614a95e7e32a6cec..90c9a0c6c2b3edb22cd01fba125c07de210b5f95 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -21,7 +21,7 @@ - if diff_file.deleted_file deleted - = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard') + = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') - if diff_file.mode_changed? %small diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 9ec6a7aa5cd7a604abc3101d8e536b16f50b5373..1bccaaf527323859820e1b676bf41b8ed318a94e 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -7,16 +7,16 @@ - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap - .frame{class: image_diff_class(diff)} - %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path} + .frame{ class: image_diff_class(diff) } + %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path } %p.image-info= "#{number_to_human_size file.size}" - else .image - %div.two-up.view + .two-up.view %span.wrap .frame.deleted - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))} - %img{src: old_file_raw_path} + %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) } + %img{ src: old_file_raw_path, alt: diff.old_path } %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" | @@ -27,8 +27,8 @@ %span.meta-height %span.wrap .frame.added - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))} - %img{src: file_raw_path} + %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) } + %img{ src: file_raw_path, alt: diff.new_path } %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" | @@ -38,32 +38,32 @@ %b H: %span.meta-height - %div.swipe.view.hide + .swipe.view.hide .swipe-frame .frame.deleted - %img{src: old_file_raw_path} + %img{ src: old_file_raw_path, alt: diff.old_path } .swipe-wrap .frame.added - %img{src: file_raw_path} + %img{ src: file_raw_path, alt: diff.new_path } %span.swipe-bar %span.top-handle %span.bottom-handle - %div.onion-skin.view.hide + .onion-skin.view.hide .onion-skin-frame .frame.deleted - %img{src: old_file_raw_path} + %img{ src: old_file_raw_path, alt: diff.old_path } .frame.added - %img{src: file_raw_path} + %img{ src: file_raw_path, alt: diff.new_path } .controls .transparent .drag-track - .dragger{:style => "left: 0px;"} + .dragger{ :style => "left: 0px;" } .opaque .view-modes.hide %ul.view-modes-menu - %li.two-up{data: {mode: 'two-up'}} 2-up - %li.swipe{data: {mode: 'swipe'}} Swipe - %li.onion-skin{data: {mode: 'onion-skin'}} Onion skin + %li.two-up{ data: { mode: 'two-up' } } 2-up + %li.swipe{ data: { mode: 'swipe' } } Swipe + %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 16c96b667141af9be80c633fe0ed5d0af3c9231e..cd18ba2ed00518078617c688a55bff87bc926367 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -16,13 +16,13 @@ - if plain = link_text - else - %a{href: "##{line_code}", data: { linenumber: link_text }} + %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" ? " " : line.new_pos - if plain = link_text - else - %a{href: "##{line_code}", data: { linenumber: link_text }} + %a{ href: "##{line_code}", data: { linenumber: link_text } } %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< - if email %pre= line.text diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 78aa9fb73919f23973aad956b43d2d6881b26473..b087485aa17648dd5a7ad4c53539d0671f93d2f2 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } +.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - last_line = 0 - diff_file.parallel_diff_lines.each do |line| @@ -13,9 +13,9 @@ - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) - %td.old_line.diff-line-num{id: left_line_code, class: left.type, data: { linenumber: left.old_pos }} - %a{href: "##{left_line_code}" }= raw(left.old_pos) - %td.line_content.parallel.noteable_line{class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old')}= diff_line_content(left.text) + %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + %a{ href: "##{left_line_code}" }= raw(left.old_pos) + %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel @@ -26,9 +26,9 @@ - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) - %td.new_line.diff-line-num{id: right_line_code, class: right.type, data: { linenumber: right.new_pos }} - %a{href: "##{right_line_code}" }= raw(right.new_pos) - %td.line_content.parallel.noteable_line{class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new')}= diff_line_content(right.text) + %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + %a{ href: "##{right_line_code}" }= raw(right.new_pos) + %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 66d6254aa1eb5abec739374703ccb5dd52cd6fd8..290f696d58270b639b46984bbe8556594b96a7e9 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -14,24 +14,23 @@ %li - if diff_file.deleted_file %span.deleted-file - %a{href: "##{file_hash}"} + %a{ href: "##{file_hash}" } %i.fa.fa-minus = diff_file.old_path - elsif diff_file.renamed_file %span.renamed-file - %a{href: "##{file_hash}"} + %a{ href: "##{file_hash}" } %i.fa.fa-minus = diff_file.old_path → = diff_file.new_path - elsif diff_file.new_file %span.new-file - %a{href: "##{file_hash}"} + %a{ href: "##{file_hash}" } %i.fa.fa-plus = diff_file.new_path - else %span.edit-file - %a{href: "##{file_hash}"} + %a{ href: "##{file_hash}" } %i.fa.fa-adjust = diff_file.new_path - diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index c0a83091c8ca618ebcf27e08771809e8803589cb..58c085cdb9d0a49d1de1b2ac38ea7ab487f77bdd 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -32,7 +32,7 @@ .empty_wrapper %h3.page-title-empty Command line instructions - %div.git-empty + .git-empty %fieldset %h5 Git global setup %pre.light-well @@ -52,7 +52,7 @@ git push -u origin master %fieldset - %h5 Existing folder or Git repository + %h5 Existing folder %pre.light-well :preserve cd existing_folder @@ -62,6 +62,15 @@ git commit git push -u origin master + %fieldset + %h5 Existing Git repository + %pre.light-well + :preserve + cd existing_repo + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git push -u origin --all + git push -u origin --tags + - if can? current_user, :remove_project, @project .prepend-top-20 = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..97de9c95de7a940e29f299444476c02d03ddb690 --- /dev/null +++ b/app/views/projects/environments/_terminal_button.html.haml @@ -0,0 +1,3 @@ +- if environment.has_terminals? && can?(current_user, :admin_environment, @project) + = link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do + = icon('terminal') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a65a630f2d0ea3c015c398eafc227628b2ac2db5..8c728eb0f6afe50b45840796843cc46380c52d3a 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -4,10 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") -.commit-icon-svg.hidden - = custom_icon("icon_commit") -.play-icon-svg.hidden - = custom_icon("icon_play") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, @@ -19,4 +15,5 @@ "help-page-path" => help_page_path("ci/environments"), "css-class" => container_class, "commit-icon-svg" => custom_icon("icon_commit"), - "play-icon-svg" => custom_icon("icon_play")}} + "terminal-icon-svg" => custom_icon("icon_terminal"), + "play-icon-svg" => custom_icon("icon_play") } } diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index bcac73d3698985b012c64f4c97da3839a76824eb..6e0d9456900b7bb8bfec6d2a5b7f9c4376a1413d 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..431253c12994233211c0478a1f3fbb643e085bcd --- /dev/null +++ b/app/views/projects/environments/terminal.html.haml @@ -0,0 +1,22 @@ +- @no_container = true +- page_title "Terminal for environment", @environment.name += render "projects/pipelines/head" + +- content_for :page_specific_javascripts do + = stylesheet_link_tag "xterm/xterm" + = page_specific_javascript_tag("terminal/terminal_bundle.js") + +%div{ class: container_class } + .top-area + .row + .col-sm-6 + %h3.page-title + Terminal for environment + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment + +.terminal-container{ class: container_class } + #terminal{ data: { project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws" } } diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 9322c82904f9cf7becbe6a58b26af19d0ed8f566..4cdb44325b30e8c06c4433621e9fa6be4d58e075 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -9,11 +9,11 @@ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path %li.file-finder - %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'} + %input#file_find.form-control.file-finder-input{ type: "text", placeholder: 'Find by path', autocomplete: 'off' } - %div.tree-content-holder + .tree-content-holder .table-holder - %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" } + %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" } %tbody = spinner nil, true diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 5ee3979c7e76e29753de557ab79f2f76e4415155..6c8a6f051a915d334591fed95588a8e24fc71e17 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,7 +1,7 @@ .top-area .nav-text - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private" - == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title} + = "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}" .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| @@ -9,7 +9,7 @@ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown - %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light sort: - if @sort.present? = sort_options_hash[@sort] 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 7f751d9ae2e87c032cee2abdb2e2d928098b541b..6d7af1685fddbebf3898b0b53a813b89fad77893 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 @@ -6,12 +6,9 @@ - stage = local_assigns.fetch(:stage, false) - coverage = local_assigns.fetch(:coverage, false) -%tr.generic_commit_status{class: ('retried' if retried)} +%tr.generic_commit_status{ class: ('retried' if retried) } %td.status - - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url) - - else - = ci_status_with_icon(generic_commit_status.status) + = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user) %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml deleted file mode 100644 index 1bba04431542e1128b7514e1a9dc970534c32193..0000000000000000000000000000000000000000 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - - if subject.target_url - = link_to subject.target_url do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name - - else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml index 195f18afc7614725774dc5697e42a58be832e1d4..bb0975a953596887beb0e72478500c380448688a 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/graphs/ci/_build_times.haml @@ -2,7 +2,7 @@ %p.light Commit duration in minutes for last 30 commits - %canvas#build_timesChart{height: 200} + %canvas#build_timesChart{ height: 200 } :javascript var data = { diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 1fbf6ca2c1ccad7a9812e362a09a8cba6a568b11..431657c4dcbedcd0c49dda35a3028f1f9a0717bb 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -13,18 +13,18 @@ %p.light Builds for last week (#{date_from_to(Date.today - 7.days, Date.today)}) - %canvas#weekChart{height: 200} + %canvas#weekChart{ height: 200 } .prepend-top-default %p.light Builds for last month (#{date_from_to(Date.today - 30.days, Date.today)}) - %canvas#monthChart{height: 200} + %canvas#monthChart{ height: 200 } .prepend-top-default %p.light Builds for last year - %canvas#yearChart.padded{height: 250} + %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| :javascript diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index ac5f792d1402dbc1b3a90fe66019d84e55ea2417..5ebb939a109ca69fc6c781c505c0f661f1e013a4 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -21,7 +21,7 @@ %h3#date_header.page-title %p.light Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. - %input#brush_change{:type => "hidden"} + %input#brush_change{ :type => "hidden" } .graphs.row #contributors-master #contributors.clearfix diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/_index.html.haml similarity index 92% rename from app/views/projects/group_links/index.html.haml rename to app/views/projects/group_links/_index.html.haml index 1b0dbbb8111aaebf8eee4dfd145e18d1957471fc..99d0df2ac34e42ef94d2f08ad70424addcd2c9d3 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/_index.html.haml @@ -20,10 +20,10 @@ .form-group = label_tag :expires_at, 'Access expiration date', class: 'label-light' .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups' %i.clear-icon.js-clear-input .help-block - On this date, all users in the group will automatically lose access to this project. + On this date, all members in the group will automatically lose access to this project. = submit_tag "Share", class: "btn btn-create" .col-lg-9.col-lg-offset-3 %hr diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index c80210d6ff46b47fa36a02b310557f5211aa278c..bd46af339cf18f540125c53dee44c0b2aabe4fec 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -43,7 +43,7 @@ = icon('clock-o') = issue.milestone.title - if issue.due_date - %span{class: "#{'cred' if issue.overdue?}"} + %span{ class: "#{'cred' if issue.overdue?}" } = icon('calendar') = issue.due_date.to_s(:medium) diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index c56b6cc11f5e40ba5798356779f7cc39c5ed8f2a..13e2150f997de12b455ef6496f41bbd9c404d1a0 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,9 +1,6 @@ - if can?(current_user, :push_code, @project) .pull-right - #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} - = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do - = icon('spinner spin') - Checking branches + #new-branch.new-branch{ 'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue) } = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do New branch diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 26f3f0ac292c105bb3bc9b1591cc3165af1eabd0..18e8372ecab67019a842486d613aa9c4825ed4d3 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,6 +6,9 @@ = content_for :sub_nav do = render "projects/issues/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") @@ -20,7 +23,6 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, @@ -30,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bd629b5c5197e1fd7e4c404fe3ce128f166c5560..9fa00811af08bcdd1cf341b6104d48a10ccfb560 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,6 +1,9 @@ +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') .clearfix.detail-page-header .issuable-header diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..605c7f61dee2355566f7451e874298e6172601d5 --- /dev/null +++ b/app/views/projects/mattermosts/_no_teams.html.haml @@ -0,0 +1,12 @@ +%p + You aren’t a member of any team on the Mattermost instance at + %strong= Gitlab.config.mattermost.host +%p + To install this service, + = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do + join a team + = icon('external-link') + and try again. +%hr +.clearfix + = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right' diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a80f9aa4c4a87de70a58a7394bf12b1607053574 --- /dev/null +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -0,0 +1,47 @@ +%p + This service will be installed on the Mattermost instance at + %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host +%hr += form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| + %h4 Team + %p + = @teams.one? ? 'The team' : 'Select the team' + where the slash commands will be used in + - selected_id = @teams.one? ? @teams.keys.first : 0 + - options = mattermost_teams_options(@teams) + - options = options_for_select(options, selected_id) + = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) + = f.hidden_field(:team_id, value: selected_id) if @teams.one? + .help-block + - if @teams.one? + This is the only available team. + - else + The list shows all available teams. + To create a team, + = link_to "#{Gitlab.config.mattermost.host}/create_team" do + use Mattermost's interface + = icon('external-link') + or ask your Mattermost system administrator. + %hr + %h4 Command trigger word + %p Choose the word that will trigger commands + = f.text_field(:trigger, value: @project.path, class: 'form-control') + .help-block + %p + Trigger word must be unique, and can't begin with a slash or contain any spaces. + Use the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + %p + Reserved: + = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do + see list of built-in slash commands + = icon('external-link') + %hr + .clearfix + .pull-right + = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg' + = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..96b1d2aee61dd72fdaf213af327d565bc29ed59d --- /dev/null +++ b/app/views/projects/mattermosts/new.html.haml @@ -0,0 +1,8 @@ +.service-installation + .inline.pull-right + = custom_icon('mattermost_logo', size: 48) + %h3 Install Mattermost Command + - if @teams.empty? + = render 'no_teams' + - else + = render 'team_selection' diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index fa189ae62d824073952a3d88b704994e99ac14ff..e3b0aa7e644edaae0944502b175cc997a2f18cd3 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,4 @@ -%li{ class: mr_css_classes(merge_request) } +%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } - if @bulk_edit .issue-check = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" @@ -39,7 +39,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.related_notes.user.count %li = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') @@ -65,7 +65,7 @@ - merge_request.labels.each do |label| = link_to_label(label, subject: merge_request.project, type: :merge_request) - + - if merge_request.tasks? %span.task-status diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 4a08ed045f4faaf87995646efc7b27d543b8e9f3..34ead6427e076851aa30f900cc8d5bab1728e81a 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -10,7 +10,7 @@ %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request = f.hidden_field :source_project_id = f.hidden_field :source_branch @@ -34,11 +34,6 @@ = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size - - if @pipeline.present? - %li.builds-tab - = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses_count %li.diffs-tab = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do Changes @@ -49,9 +44,6 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - # This tab is always loaded via AJAX - - if @pipeline.present? - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" - if @pipelines.any? #pipelines.pipelines.tab-pane = render "projects/merge_requests/show/pipelines" @@ -66,6 +58,5 @@ }); :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}" + action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 896f10104fa6f7dc3749fccaa5ed58d1da4d97c4..2a7cd3a19d0bb6d117f1526ee07f0f3949962a4c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,13 +1,15 @@ +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') -.merge-request{'data-url' => merge_request_path(@merge_request)} +.merge-request{ 'data-url' => merge_request_path(@merge_request) } = render "projects/merge_requests/show/mr_title" - .merge-request-details.issuable-details{data: {id: @merge_request.project.id}} + .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } = render "projects/merge_requests/show/mr_box" .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? @@ -41,71 +43,63 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default.append-bottom-default + .merge-manually.light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - if @commits_count.nonzero? - .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - %div{ class: container_class } - %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - - if @pipeline.present? - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses_count - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - %div - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved - = render "discussions/jump_to_next" - - .tab-content#diff-notes-app - #notes.notes.tab-pane.voting_notes - .content-block.content-block-small - = render 'award_emoji/awards_block', awardable: @merge_request, inline: true - - .row - %section.col-md-12 - .issuable-discussion - = render "projects/merge_requests/discussion" - - #commits.commits.tab-pane - - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - - # This tab is always loaded via AJAX - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX - - .mr-loading-status - = spinner + .content-block.content-block-small.emoji-list-container + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true + + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + %div{ class: container_class } + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + %div + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved + = render "discussions/jump_to_next" + + .tab-content#diff-notes-app + #notes.notes.tab-pane.voting_notes + .row + %section.col-md-12 + .issuable-discussion + = render "projects/merge_requests/discussion" + + #commits.commits.tab-pane + - # This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + - # This tab is always loaded via AJAX + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX + + .mr-loading-status + = spinner = render 'shared/issuable/sidebar', issuable: @merge_request - if @merge_request.can_be_reverted?(current_user) @@ -119,3 +113,6 @@ merge_request = new MergeRequest({ action: "#{controller.action_name}" }); + + var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; + diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 16789f68f701a9d3016bb565ba882cd40419d9cf..ebef2157d34b1f99957d3369eaba3e4fc6afc021 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,5 +1,6 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" @@ -9,29 +10,29 @@ = render 'shared/issuable/sidebar', issuable: @merge_request -#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json), +#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json), resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } } - .loading{"v-if" => "isLoading"} + .loading{ "v-if" => "isLoading" } %i.fa.fa-spinner.fa-spin - .nothing-here-block{"v-if" => "hasError"} + .nothing-here-block{ "v-if" => "hasError" } {{conflictsData.errorMessage}} = render partial: "projects/merge_requests/conflicts/commit_stats" - .files-wrapper{"v-if" => "!isLoading && !hasError"} + .files-wrapper{ "v-if" => "!isLoading && !hasError" } .files - .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"} + .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" } .file-title - %i.fa.fa-fw{":class" => "file.iconClass"} + %i.fa.fa-fw{ ":class" => "file.iconClass" } %strong {{file.filePath}} = render partial: 'projects/merge_requests/conflicts/file_actions' .diff-content.diff-wrap-lines - .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" - .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } %parallel-conflict-lines{ ":file" => "file" } - %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"} + %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" } = render partial: "projects/merge_requests/conflicts/components/diff_file_editor" = render partial: "projects/merge_requests/conflicts/submit_form" diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 5ab3cd96163c3d623fe704ae406c616a813d3fdb..964dc40a21377edd8425eace3a7528c275a0f4b5 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -1,9 +1,9 @@ -.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} - .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"} +.content-block.oneline-block.files-changed{ "v-if" => "!isLoading && !hasError" } + .inline-parallel-buttons{ "v-if" => "showDiffViewTypeSwitcher" } .btn-group - %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"} + %button.btn{ ":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')" } Inline - %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"} + %button.btn{ ":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')" } Side-by-side .js-toggle-container diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 05af57acf038bfedf4a491024c78fe581218ad6b..2595ce74ac0ee2a8c090346273a16b02a34e5308 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -1,5 +1,5 @@ .file-actions - .btn-group{"v-if" => "file.type === 'text'"} + .btn-group{ "v-if" => "file.type === 'text'" } %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }", '@click' => "onClickResolveModeButton(file, 'interactive')", type: 'button' } @@ -8,5 +8,5 @@ '@click' => "onClickResolveModeButton(file, 'edit')", type: 'button' } Edit inline - %a.btn.view-file.btn-file-option{":href" => "file.blobPath"} + %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } View file @{{conflictsData.shortCommitSha}} diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 6ffaa9ad4d226dce4c99c3722fec9cb1a74a717b..62c9748c5102257b8a6f8eca4c256531a467fb5c 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -10,7 +10,7 @@ .col-sm-offset-2.col-sm-10 .row .col-xs-6 - %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" } + %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" } %span {{commitButtonText}} .col-xs-6.text-right = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml index 3c927d362c28612641a7cc4bda453544b861dde2..aff3fb82fa6b4f4f721944885a436a2a1ed36b2a 100644 --- a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml +++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml @@ -1,4 +1,4 @@ -%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"} +%diff-file-editor{ "inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation" } .diff-editor-wrap{ "v-show" => "file.showEditor" } .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } .discard-changes-alert diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml index d35c7bee163d67571e2a7491579e51855fcf2a5d..d828cb6cf9e78c19090d962c65989c71327a3861 100644 --- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml +++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml @@ -1,14 +1,14 @@ -%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"} +%inline-conflict-lines{ "inline-template" => "true", ":file" => "file" } %table - %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} - %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %tr.line_holder.diff-inline{ "v-for" => "line in file.inlineLines" } + %td.diff-line-num.new_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" } %a {{line.new_line}} - %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %td.diff-line-num.old_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" } %a {{line.old_line}} - %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText"} - %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %strong{"v-html" => "line.richText"} + %td.line_content{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText" } + %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" } + %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" } + %td.line_content.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" } + %strong{ "v-html" => "line.richText" } %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml deleted file mode 100644 index 808ef7fed27d876911458b94ec8c8a2562f0abec..0000000000000000000000000000000000000000 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a0e12fb3f38340118a7443fd6b17a9af1c8a6c27..11793919ff7b8978634b5be869363dea93f31678 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,6 +1,8 @@ -.content-block.oneline-block - = icon("sort-amount-desc") - Most recent commits displayed first - -%ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch +- if @commits.empty? + .commits-empty + %h4 + There are no commits yet. + = custom_icon ('illustration_no_commits') +- else + %ol#commits-list.list-unstyled + = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 99c71e1454a8dea52e53c92c9066ae1ecf5a3523..5f048d04b278ef762248d79ac058280bf05fbf43 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,13 +1,5 @@ -- if @merge_request_diff.collected? +- if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/show/versions' = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} -- else - .alert.alert-warning - %h4 - Changes view for this comparison is extremely large. - %p - You can - = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink" - instead. diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index f1d5441f9ddecf78bd75350787ba9f3aea7a612d..93ed4b68e0e83a8b73e705a8db65cd588e5fc07e 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -1,14 +1,14 @@ -%div#modal_merge_info.modal +#modal_merge_info.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Check out, review, and merge locally .modal-body %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button(clipboard_target: "pre#merge-info-1") + = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard") %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(clipboard_target: "pre#merge-info-3") + = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard") %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(clipboard_target: "pre#merge-info-4") + = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index ed23d06ee5e06b6fa6d16624700eb0cb91321398..683cb8a5a27ae9db5896cee90be6073a7470d9a6 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -4,7 +4,7 @@ %div - if @merge_request.description.present? - .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} + .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } .wiki = preserve do = markdown_field(@merge_request, :description) diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 5cc92595fe06285360f594359be9e6c8b5872425..b0f3c86fd210b65afe0acfcde294b7db57b0deb2 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -1,6 +1,6 @@ - if @merge_request_diffs.size > 1 .mr-version-controls - %div.mr-version-menus-container.content-block + .mr-version-menus-container.content-block Changes between %span.dropdown.inline.mr-version-dropdown %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} } @@ -13,13 +13,13 @@ .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Version: - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times', class: 'dropdown-menu-close-icon') .dropdown-content %ul - @merge_request_diffs.each do |merge_request_diff| %li - = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do + = link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do %strong - if merge_request_diff.latest? latest version @@ -43,7 +43,7 @@ .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Compared with: - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times', class: 'dropdown-menu-close-icon') .dropdown-content %ul diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml index f3cc0e7e8a19fe04e1985c1b2d44c208dfaec37e..15f47ecf21014d54082375975623a2e30ba97e2a 100644 --- a/app/views/projects/merge_requests/widget/_closed.html.haml +++ b/app/views/projects/merge_requests/widget/_closed.html.haml @@ -6,7 +6,7 @@ - if @merge_request.closed_event by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} - %p + %p = succeed '.' do The changes were not merged into %span.label-branch= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 9ab7971b56cf053361d7e02af81a6f9e49f58511..c80dc33058d3d60d21779222c527069c964479b6 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -17,7 +17,7 @@ - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} + .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } = ci_icon_for_status(status) %span CI build @@ -32,11 +32,11 @@ = icon("spinner spin") Checking CI status for #{@merge_request.diff_head_commit.short_id}… - .ci_widget.ci-not_found{style: "display:none"} + .ci_widget.ci-not_found{ style: "display:none" } = icon("times-circle") Could not find CI status for #{@merge_request.diff_head_commit.short_id}. - .ci_widget.ci-error{style: "display:none"} + .ci_widget.ci-error{ style: "display:none" } = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index d836a2535070f86db76500611509b50de4126492..9eef011b5910eef92d54a4489d94a08808b1981b 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -5,10 +5,10 @@ - if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .clearfix.merged-buttons - if can_remove_source_branch - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted - = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") - if mr_can_be_cherry_picked - = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index eee711dc5afc73041ecdc528eaf2629942c0f91c..c0d6ab669b812602a37395865a8559fa6168f26b 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -30,11 +30,16 @@ - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' - - if mr_closes_issues.present? + - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present? .mr-widget-footer %span - %i.fa.fa-check - Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} - = succeed '.' do - != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author - = mr_assign_issues_link + = icon('check') + - if mr_closes_issues.present? + Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} + = succeed '.' do + != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author + = mr_assign_issues_link + - if mr_issues_mentioned_but_not_closing.present? + #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} + != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author + #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed. diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a8918c85dde36f4957a59fbc1b11a857a29cd664..38328501ffd4ed6c612483ceb2ba8dd4cdc5320b 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -24,12 +24,10 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, - builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; if (typeof merge_request_widget !== 'undefined') { - clearInterval(merge_request_widget.fetchBuildStatusInterval); merge_request_widget.cancelPolling(); merge_request_widget.clearEventListeners(); } diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 435fe835fae6e6358e627aa96561c04c04e6138c..7809e9c8c72fa318f5e935d6e86bc1d8f0be8d51 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,3 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + - status_class = @pipeline ? " ci-#{@pipeline.status}" : nil = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| @@ -41,25 +44,9 @@ Modify commit message .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, + message_with_description: @merge_request.merge_commit_message(include_description: true), + message_without_description: @merge_request.merge_commit_message, text: @merge_request.merge_commit_message, rows: 14, hint: true = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" - - :javascript - $('.accept-mr-form').on('ajax:send', function() { - $(".accept-mr-form :input").disable(); - }); - - $('.accept_merge_request').on('click', function() { - $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); - }); - - $('.merge_when_build_succeeds').on('click', function() { - $("#merge_when_build_succeeds").val("1"); - }); - - $('.js-merge-dropdown a').on('click', function(e) { - e.preventDefault(); - $(this).closest("form").submit(); - }); diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml index ab30fa6b24365bef35d6775d0963ddf6e250e3ce..0d61e56d8fbfd879cd735ac2ea4fd056fc0334fd 100644 --- a/app/views/projects/merge_requests/widget/open/_archived.html.haml +++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml @@ -1,4 +1,4 @@ -%h4 +%h4 Project is archived %p This merge request cannot be merged because archived projects cannot be written to. diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index e16878ba51383bab3bfc837d4a88ed4e26a0c2a3..500867674465f3dbac86896fb638e5e6c6de984c 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -1,9 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + %strong = icon("spinner spin") Checking ability to merge automatically… - -:javascript - $(function() { - merge_request_widget.getMergeStatus(); - }); - diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index af3096f04d97d4c43d93d09b7d437bdc811a87fd..c98b2c425977f740b907e61b6864032b5ee0eb42 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -1,21 +1,23 @@ +- can_resolve = @merge_request.conflicts_can_be_resolved_by?(current_user) +- can_resolve_in_ui = @merge_request.conflicts_can_be_resolved_in_ui? +- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user) + %h4.has-conflicts = icon("exclamation-triangle") This merge request contains merge conflicts %p - Please - - if @merge_request.conflicts_can_be_resolved_by?(current_user) - - if @merge_request.conflicts_can_be_resolved_in_ui? - = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - - else - %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"} - resolve these conflicts locally - - else - resolve these conflicts - + To merge this request, resolve these conflicts + - if can_resolve && !can_resolve_in_ui + locally or + - unless can_merge + ask someone with write access to this repository to + merge it locally. - - if @merge_request.can_be_merged_via_command_line_by?(current_user) - #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. - - else - ask someone with write access to this repository to merge this request manually. +- if (can_resolve && can_resolve_in_ui) || can_merge + .btn-group + - if can_resolve && can_resolve_in_ui + = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn" + - if can_merge + = link_to "Merge locally", "#modal_merge_info", class: "btn how_to_merge_link vlink", "data-toggle" => "modal" diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml index 35626b624b7a2cb9de503aa314899455e3be3983..7af8c01c1346a13960dfc8e715d9c7894ce465d3 100644 --- a/app/views/projects/merge_requests/widget/open/_nothing.html.haml +++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml @@ -1,4 +1,4 @@ -%h4 +%h4 = icon("exclamation-triangle") Nothing to merge from %span.label-branch= source_branch_with_namespace(@merge_request) diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 513710e8e66fb6697a2023b446611a742e9169eb..0f4a850875155e30c224fc1af6d723435be7b83b 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -20,6 +20,6 @@ - if @milestone.new_record? = f.submit 'Create milestone', class: "btn-create btn" = link_to "Cancel", namespace_project_milestones_path(@project.namespace, @project), class: "btn btn-cancel" - -else + - else = f.submit 'Save changes', class: "btn-save btn" = link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 0788924d44a0a78c060e0ff72399ecd7912b8e3c..064e92b15eba1d7e12021569e0f2b60934bb7c7e 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -68,11 +68,16 @@ - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_url, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do = icon('git', text: 'Repo by URL') - %div{ class: 'import_gitlab_project' } + .import_gitlab_project - if gitlab_project_import_enabled? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = icon('gitlab', text: 'GitLab export') diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 8620f4922821725f50521b6ca467a010a669c36b..e8e450742b557a712959762a66c6ed185abd93e5 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,11 +1,14 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| - = note_target_fields(note) - = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." + = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do + = hidden_field_tag :target_id, '', class: 'js-form-target-id' + = hidden_field_tag :target_type, '', class: 'js-form-target-type' + = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do + = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix - = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' + .settings-message.note-edit-warning.js-edit-warning + Finish editing this message first! + = submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } Cancel diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 46b402545cddb85d20cc75acf19ddd48da801634..b561052e721b76e64422841bfa27d6666b0522ce 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -3,6 +3,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type + = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha) = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code @@ -23,5 +24,5 @@ .note-form-actions.clearfix = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button" = yield(:note_actions) - %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}} + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } Discard draft diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 778a32e63453659ace9b284ead7ffa9542397620..36c388c3318ae2505b39a9082531e600f6cc403c 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,17 +5,17 @@ %li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon - %a{href: user_path(note.author)} + %a{ href: user_path(note.author) } = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header = link_to_member(note.project, note.author, avatar: false) - .inline.note-headline-light + .note-headline-light = note.author.to_reference - unless note.system commented - if note.system - %span{class: 'system-note-message'} + %span.system-note-message = note.redacted_note_html %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') @@ -61,13 +61,15 @@ = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do = icon('trash-o', class: 'danger-highlight') - .note-body{class: note_editable ? 'js-task-list-container' : ''} + .note-body{ class: note_editable ? 'js-task-list-container' : '' } .note-text.md = preserve do = note.redacted_note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable - = render 'projects/notes/edit_form', note: note + .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} + %textarea.hidden.js-task-list-field.original-task-list #{note.note} .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 00b62a595ff213fe190071003c1868f6cb5fdf06..fbd2bff5bbb27a7bff6f1daf572b697535b51295 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,5 +1,8 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" + += render 'projects/notes/edit_form' + %ul.notes.notes-form.timeline %li.timeline-entry .flash-container.timeline-content @@ -20,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 229bdfb0e8d983c9f996d92837ec9253cdefac45..ca76f13ef5ead69fa6d122e961802efb821672ec 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.detailed_status) + = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) @@ -23,7 +23,7 @@ .info-well - if @commit.status .well-segment.pipeline-info - %div{class: "icon-container ci-status-icon-#{@commit.status}"} + %div{ class: "icon-container ci-status-icon-#{@commit.status}" } = ci_icon_for_status(@commit.status) = pluralize @pipeline.statuses.count(:id), "build" - if @pipeline.ref diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a0b14a7274a24a198ca92f1cee60660dc5d12911 --- /dev/null +++ b/app/views/projects/pipelines/_stage.html.haml @@ -0,0 +1,3 @@ +- @stage.statuses.latest.each do |status| + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index e1e787dbde434306b6bc78ee63d2ad1fe8a426cf..df36279ed75969000c10774c8f085f6fbbdcbe08 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,23 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{class: ('active' if @scope.nil?)} + %li{ class: ('active' if @scope.nil?) }> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{class: ('active' if @scope == 'running')} + %li{ class: ('active' if @scope == 'running') }> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count = number_with_delimiter(@running_or_pending_count) - %li{class: ('active' if @scope == 'branches')} + %li{ class: ('active' if @scope == 'branches') }> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{class: ('active' if @scope == 'tags')} + %li{ class: ('active' if @scope == 'tags') }> = link_to project_pipelines_path(@project, scope: :tags) do Tags @@ -35,21 +35,33 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - - %div.content-list.pipelines + .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - if @pipelines.blank? %div .nothing-here-block No pipelines to show - else - .table-holder - %table.table.ci-table - %thead - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th.hidden-xs - = render @pipelines, commit_sha: true, stage: true, allow_retry: true - - = paginate @pipelines, theme: 'gitlab' + .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), + } } + + .vue-pipelines-index + += page_specific_javascript_tag('vue_pipelines_index/index.js') diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 29a41bc664bc7f792f426f997e4111058fa120cf..49c1d88642329752938c110107f3d85563c4383a 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -2,7 +2,7 @@ - page_title "Pipeline" = render "projects/pipelines/head" -%div.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } } +.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } } - if @commit = render "projects/pipelines/info" diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..ab0771b5751c3d4c8ab41ab5d8a0b73c21c3d9a6 --- /dev/null +++ b/app/views/projects/project_members/_index.html.haml @@ -0,0 +1,22 @@ +.row.prepend-top-default + .col-lg-3.settings-sidebar + %h4.prepend-top-0 + Members + - if can?(current_user, :admin_project_member, @project) + %p + Add a new member to + %strong= @project.name + .col-lg-9 + .light.prepend-top-default + - if can?(current_user, :admin_project_member, @project) + = render "projects/project_members/new_project_member" + + = render 'shared/members/requests', membership_source: @project, requesters: @requesters + .append-bottom-default.clearfix + %h5.member.existing-title + Existing members and groups + - if @group_links.any? + = render 'projects/project_members/groups', group_links: @group_links + + = render 'projects/project_members/team', members: @project_members + = paginate @project_members, theme: "gitlab" 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 79dcd7a6ee9acb501ef63897603233be50dcbd0d..2b1c23f7dda85be6d0eab1da9bdbd7f068e834ad 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,22 +1,18 @@ = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| - .row - .col-md-4.col-lg-6 - = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true) - .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. - - .col-md-3.col-lg-2 - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" - .help-block.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions"), class: "vlink" - about role permissions - - .col-md-3.col-lg-2 - .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' - %i.clear-icon.js-clear-input - .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this project. - - .col-md-2 - = f.submit "Add to project", class: "btn btn-create btn-block" + .form-group + = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") + .help-block.append-bottom-10 + Search for members by name, username, or email, or invite new ones using their email address. + .form-group + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions + .form-group + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' + %i.clear-icon.js-clear-input + .help-block.append-bottom-10 + On this date, the member(s) will automatically lose access to this project. + = f.submit "Add to project", class: "btn btn-create" + = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project" diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index c1e894d8f40f1395c5c2dc46800634a21a4c44d9..5292e73be7af837289dfa7b8ca116baebb0918c0 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,7 +1,13 @@ .panel.panel-default .panel-heading - Users with access to + Members with access to %strong #{@project.name} %span.badge= @project_members.total_count + = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + = render 'shared/members/sort_dropdown' %ul.content-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index eef97107d7710db73515b0493e0d32cb2a2f95d8..42ce4f8001b8dce7980fe4f36d916498f4a80646 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -12,5 +12,4 @@ .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel" - + = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml deleted file mode 100644 index bdeb704b6daa6a7c01393f5aab0f5778a168c185..0000000000000000000000000000000000000000 --- a/app/views/projects/project_members/index.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- page_title "Members" - -.project-members-page.prepend-top-default - %h4.project-members-title.clearfix - Members - = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project" - - if can?(current_user, :admin_project_member, @project) - .project-members-new.append-bottom-default - %p.clearfix - Add new user to - %strong= @project.name - = render "new_project_member" - - = render 'shared/members/requests', membership_source: @project, requesters: @requesters - - .append-bottom-default.clearfix - %h5.member.existing-title - Existing users and groups - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") - - if @group_links.any? - = render 'groups', group_links: @group_links - - = render 'team', members: @project_members - = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 6e58e5a0c781a0c4a211c0a58b3000026abb6b20..7036b8a5ccce4306f5febdd22cd3df035f1453c7 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -1,4 +1,4 @@ -%li.runner{id: dom_id(runner)} +%li.runner{ id: dom_id(runner) } %h4 = runner_status_icon(runner) %span.monospace diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index db51c4f8a4e2b54a8c5032506a597f1040ae2d16..fc338dcf887cbc3dc5821d51aa57ccd7ac6e2641 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -8,7 +8,6 @@ .col-lg-9 = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| = render 'shared/service_settings', form: form, subject: @service - .footer-block.row-content-block = form.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..8ca4c51a06426917e2c29061e5175318b5c105f3 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -0,0 +1,91 @@ +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" + +To setup this service: +%ul.list-unstyled + %li + 1. + = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + on your Mattermost installation + %li + 2. + = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' + in Mattermost with these options: + +%hr + +.help-form + .form-group + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#display_name') + + .form-group + = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#description') + + .form-group + = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + + .form-group + = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#request_url') + + .form-group + = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_username') + + .form-group + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_icon') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Yes + + .form-group + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_hint') + + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') + +%hr + +%ul.list-unstyled + %li + 3. After adding the slash command, paste the + + %strong token + into the field below diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index a676c0290a090c8ba510c2bc0c3a38162a9510f4..63b797cd3913ca1575d2eeb8b237d09b052c755d 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,5 +1,4 @@ -- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" -- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}" +- enabled = Gitlab.config.mattermost.enabled .well This service allows GitLab users to perform common operations on this @@ -8,93 +7,9 @@ See list of available commands in Mattermost after setting up this service, by entering %code /<command_trigger_word> help - %br - %br - To setup this service: - %ul.list-unstyled - %li - 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' - on your Mattermost installation - %li - 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - - %hr - - .help-form - .form-group - = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#display_name') - - .form-group - = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#description') - - .form-group - = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace - - .form-group - = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#request_url') - - .form-group - = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST - - .form-group - = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_username') - - .form-group - = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_icon') - - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Yes - - .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_hint') - - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') - %hr + - unless enabled + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service - %ul.list-unstyled - %li - 3. After adding the slash command, paste the - %strong token - into the field below +- if enabled + = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..c929eee3bb92425c31a388fdc484320826c7c055 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -0,0 +1,7 @@ +.services-installation-info + - unless @service.activated? + .row + .col-sm-9.col-sm-offset-3 + = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do + = custom_icon('mattermost_logo', size: 15) + = 'Add to Mattermost' diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..6d7c2defe2b18304053090cf42c0f6c028d4ed9d --- /dev/null +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -0,0 +1,93 @@ +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" + +.well + This service allows GitLab users to perform common operations on this + project by entering slash commands in Slack. + %br + See list of available commands in Slack after setting up this service, + by entering + %code /<command> help + %br + %br + To setup this service: + %ul.list-unstyled + %li + 1. + = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' + in your Slack team with these options: + + %hr + + .help-form + .form-group + = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + + .form-group + = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#url') + + .form-group + = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#customize_name') + + .form-group + = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) + = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list + + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') + + .form-group + = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_usage_hint') + + .form-group + = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#descriptive_label') + + %hr + + %ul.list-unstyled + %li + 2. Paste the + %strong Token + into the field below + %li + 3. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Slack! diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..d81ed7bb6097af815cb5fc6d40e47037e5082365 --- /dev/null +++ b/app/views/projects/settings/members/show.html.haml @@ -0,0 +1,6 @@ +- page_title "Members" + += render "projects/project_members/index" +- if can?(current_user, :admin_project, @project) + - if @project.allowed_to_share_with_group? + = render "projects/group_links/index" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c50093cf47caec4449e6fb57d7f6a40437487b51..80d4081dd7bd06fe25bd104d1fa51e178293d043 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -17,10 +17,10 @@ %ul.nav %li = link_to project_files_path(@project) do - Files (#{repository_size}) + Files (#{storage_counter(@project.statistics.total_repository_size)}) %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) + #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) %li = link_to namespace_project_branches_path(@project.namespace, @project) do #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) @@ -64,20 +64,15 @@ - unless @repository.gitlab_ci_yml %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set Up CI - - %li.project-repo-buttons.right - .project-right-buttons - - if current_user - = render 'shared/members/access_request_buttons', source: @project - = render "projects/buttons/koding" - - .btn-group.project-repo-btn-group - = render 'projects/buttons/download', project: @project, ref: @ref - = render 'projects/buttons/dropdown' + Set up CI + - if koding_enabled? && @repository.koding_yml.blank? + %li.missing + = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + Set up auto deploy - .pull-right - = render 'shared/notifications/button', notification_setting: @notification_setting - if @repository.commit .project-last-commit{ class: container_class } = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 9503dbded13ae43766c19c3f64b52e5811832aef..485b23815bc022ceca2a07bf1684ba6fddce2f31 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -8,10 +8,11 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' - = render 'award_emoji/awards_block', awardable: @snippet, inline: true + .row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true - %div#notes= render "projects/notes/notes_with_form" + #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index 1d8fa10db0c886096c23030a392e054eb6397370..4ee30b023acc116650192cb163f404cec85cb0ef 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -1,6 +1,6 @@ - stage = local_assigns.fetch(:stage) - statuses = stage.statuses.latest -- status_groups = statuses.sort_by(&:name).group_by(&:group_name) +- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name) %li.stage-column .stage-name %a{ name: stage.name } @@ -10,13 +10,10 @@ - status_groups.each do |group_name, grouped_statuses| - if grouped_statuses.one? - status = grouped_statuses.first - - is_playable = status.playable? && can?(current_user, :update_build, @project) - %li.build{ class: ("playable" if is_playable) } + %li.build{ 'id' => "ci-badge-#{group_name}" } .curve - .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/graph_badge', subject: status - else - %li.build + %li.build{ 'id' => "ci-badge-#{group_name}" } .curve - .dropdown.inline.build-content - = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses + = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 2b26ad9d6fa2f8287c777f676257bc6f21dfb7e2..9c5eb501174c1bff0cf5bd97f5014aa5a78bf684 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -1,13 +1,14 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } - %span{class: "ci-status-icon ci-status-icon-#{group_status}"} +%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } + %span{ class: "ci-status-icon ci-status-icon-#{group_status}" } = ci_icon_for_status(group_status) %span.ci-status-text = name - %span.badge= subject.size -.dropdown-menu.grouped-pipeline-dropdown + %span.dropdown-counter-badge= subject.size + +%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown .arrow - %ul + .scrollable-menu - subject.each do |status| %li - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml index 1684e02fbad5af60d363969529b10a01924ed08d..28e1c060875eb28ea0fbd0bf2c4261decf4956e6 100644 --- a/app/views/projects/stage/_stage.html.haml +++ b/app/views/projects/stage/_stage.html.haml @@ -1,13 +1,13 @@ %tr - %th{colspan: 10} + %th{ colspan: 10 } %strong %a{ name: stage.name } - %span{class: "ci-status-link ci-status-icon-#{stage.status}"} + %span{ class: "ci-status-link ci-status-icon-#{stage.status}" } = ci_icon_for_status(stage.status) = stage.name.titleize = render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true = render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true %tr - %td{colspan: 10} + %td{ colspan: 10 } diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index c42641afea0cbaffcdc6ee57a389a650967c6a91..8ef069b9e057f2a2f4d9397087cc372388153769 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(tag.dereferenced_target) - release = @releases.find { |release| release.tag == tag.name } -%li - %div +%li.flex-row + .row-main-content.str-truncated = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do %span.item-title = icon('tag') @@ -10,24 +10,25 @@ = strip_gpg_signature(tag.message) - .controls - = render 'projects/buttons/download', project: @project, ref: tag.name + - if commit + .block-truncated + = render 'projects/branches/commit', commit: commit, project: @project + - else + %p + Cant find HEAD commit for this tag + - if release && release.description.present? + .description.prepend-top-default + .wiki + = preserve do + = markdown_field(release, :description) - - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do - = icon("pencil") + .row-fixed-content.controls + = render 'projects/buttons/download', project: @project, ref: tag.name - - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do - = icon("trash-o") + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do + = icon("pencil") - - if commit - = render 'projects/branches/commit', commit: commit, project: @project - - else - %p - Cant find HEAD commit for this tag - - if release && release.description.present? - .description.prepend-top-default - .wiki - = preserve do - = markdown_field(release, :description) + - if can?(current_user, :admin_project, @project) + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = icon("trash-o") diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 1d39f3a75345c2290c5749ad433164a5d5ddabff..e2f132f77428c7551cf9cba226347b9915a3818c 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -2,16 +2,16 @@ - page_title "Tags" = render "projects/commits/head" -%div{ class: container_class } - .top-area - .nav-text +.flex-list{ class: container_class } + .top-area.flex-row + .nav-text.row-main-content Tags give the ability to mark specific points in history as being important - .nav-controls + .nav-controls.row-fixed-content = form_tag(filter_tags_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } - .dropdown.inline + .dropdown %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } %span.light = projects_sort_options_hash[@sort] @@ -30,7 +30,7 @@ .tags - if @tags.any? - %ul.content-list + %ul.flex-list.content-list = render partial: 'tag', collection: @tags = paginate @tags, theme: 'gitlab' diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index c06a413eb2ff45094a64f3de2530aaae51bb5712..160d4c7a22372d82e415c67b5ad19065ed36e24d 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -2,7 +2,7 @@ - if @error .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + %button.close{ type: "button", "data-dismiss" => "alert" } × = @error %h3.page-title diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 12facb6eb73dc0cfcf66f9479346db3e6bd560ae..fad3c5c2173f31b39ee8459af0f3d205d9d57ca7 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -12,21 +12,23 @@ - else Cant find HEAD commit for this tag - .nav-controls + .nav-controls.controls-flex - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do = icon("pencil") - = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse files' do + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do = icon('files-o') - = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do = icon('history') - = render 'projects/buttons/download', project: @project, ref: @tag.name + .btn-container.controls-item + = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) - .pull-right + .btn-container.controls-item-full = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o + - if @tag.message.present? - %pre.body + %pre.wrap = strip_gpg_signature(@tag.message) .append-bottom-default.prepend-top-default diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index ee417b58cbf5656706351150bff0d1b3a9628571..425b460eb093a7af430a3925e5cdc7a82dac4c8f 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -6,4 +6,4 @@ %span.str-truncated= file_name %td.hidden-xs.tree-commit %td.tree-time-ago.cgray.text-right - = render 'projects/tree/spinner' \ No newline at end of file + = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 2b5f671c09ec5e4a1e268ecaa20f1578119c650d..04d52361db0ac67c0b61b6be48fc28d4ffb5d74a 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -1,4 +1,4 @@ -%tr{ class: "tree-item" } +%tr.tree-item %td.tree-item-file-name %i.fa.fa-archive.fa-fw = submodule_link(submodule_item, @ref) diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index d37c376c36b375d265ecbb27e75d4de3153a6a4b..2c08221565b6b3a484db374867074a9b38d81051 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,14 +1,14 @@ -%div.tree-content-holder +.tree-content-holder .table-holder - %table.table#tree-slider{class: "table_#{@hex_path} tree-table" } + %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } %thead %tr %th Name %th.hidden-xs .pull-left Last commit .last-commit.hidden-sm.pull-left - %small.light - = clipboard_button(clipboard_text: @commit.id) + %small.light + = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" = time_ago_with_tooltip(@commit.committed_date) = @commit.full_title diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 1c5f8b3928b3385bc85406ee2d4c151f2a70269b..259207a6dfde290955bc0873513dae730901aa90 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -15,11 +15,11 @@ - if current_user %li - if !on_top_of_branch? - %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} + %span.btn.add-to-tree.disabled.has-tooltip{ title: "You can only add files when you are on a branch", data: { container: 'body' } } = icon('plus') - else %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown" } = icon('plus') %ul.dropdown-menu - if can_edit_tree? @@ -28,11 +28,11 @@ = icon('pencil fw') New file %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do = icon('file fw') Upload file %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do = icon('folder fw') New directory - elsif can?(current_user, :fork_project, @project) diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 1ccef6d52abb35e6273531ebd7202ff2e6263efd..15c9536133cd24b62924d0e2a20b8d6cd35f4572 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -6,4 +6,4 @@ %span.str-truncated= path %td.hidden-xs.tree-commit %td.tree-time-ago.text-right - = render 'projects/tree/spinner' \ No newline at end of file + = render 'projects/tree/spinner' diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml index 3930370013184f87ea270ab39b8d6187cec52f18..cf7ae0b489f35cca2f4f72a94c6f468670f23b1f 100644 --- a/app/views/projects/variables/index.html.haml +++ b/app/views/projects/variables/index.html.haml @@ -15,4 +15,4 @@ No variables found, add one with the form above. - else = render "table" - %button.btn.btn-info.js-btn-toggle-reveal-values{"data-status" => 'hidden'} Reveal Values + %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index c32cb122c26fe33b8e4f263fff88275f7033fab2..c74f53b4c39fed8f7af6f81c8ea4fb008a764e3d 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,11 +1,11 @@ - @no_container = true %div{ class: container_class } - %div#modal-new-wiki.modal + #modal-new-wiki.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title New Wiki Page .modal-body %form.new-wiki-page diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml index a585147ddd18fd36194b0f52e0f612e460258ee5..94e5a5d9709997465d203cd82553f8c44e60d4ad 100644 --- a/app/views/repository_check_mailer/notify.html.haml +++ b/app/views/repository_check_mailer/notify.html.haml @@ -2,7 +2,7 @@ #{@message}. %p - = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) + = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1) %p You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml index 93db151329efecc297ef3be4c8b1700b3ecccbda..0902c50d0527e96944fa2c45da1d3b7965d2bf2e 100644 --- a/app/views/repository_check_mailer/notify.text.haml +++ b/app/views/repository_check_mailer/notify.text.haml @@ -1,6 +1,6 @@ #{@message}. \ -View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} +View details: #{admin_projects_url(last_repository_check_failed: 1)} You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 2c378231237393072f2ddfaa3c8799e57bdb169f..8cbecb725b528aae1abd8caf9a455e5a7e4f0e63 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,70 +1,70 @@ %ul.nav-links.search-filter - if @project - %li{class: ("active" if @scope == 'blobs')} + %li{ class: ("active" if @scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do Code %span.badge = @search_results.blobs_count - %li{class: ("active" if @scope == 'issues')} + %li{ class: ("active" if @scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} + %li{ class: ("active" if @scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge = @search_results.merge_requests_count - %li{class: ("active" if @scope == 'milestones')} + %li{ class: ("active" if @scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge = @search_results.milestones_count - %li{class: ("active" if @scope == 'notes')} + %li{ class: ("active" if @scope == 'notes') } = link_to search_filter_path(scope: 'notes') do Comments %span.badge = @search_results.notes_count - %li{class: ("active" if @scope == 'wiki_blobs')} + %li{ class: ("active" if @scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do Wiki %span.badge = @search_results.wiki_blobs_count - %li{class: ("active" if @scope == 'commits')} + %li{ class: ("active" if @scope == 'commits') } = link_to search_filter_path(scope: 'commits') do Commits %span.badge = @search_results.commits_count - elsif @show_snippets - %li{class: ("active" if @scope == 'snippet_blobs')} + %li{ class: ("active" if @scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do Snippet Contents %span.badge = @search_results.snippet_blobs_count - %li{class: ("active" if @scope == 'snippet_titles')} + %li{ class: ("active" if @scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do Titles and Filenames %span.badge = @search_results.snippet_titles_count - else - %li{class: ("active" if @scope == 'projects')} + %li{ class: ("active" if @scope == 'projects') } = link_to search_filter_path(scope: 'projects') do Projects %span.badge = @search_results.projects_count - %li{class: ("active" if @scope == 'issues')} + %li{ class: ("active" if @scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} + %li{ class: ("active" if @scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge = @search_results.merge_requests_count - %li{class: ("active" if @scope == 'milestones')} + %li{ class: ("active" if @scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 000532b1c9a8f8ae71a271345c35eb28bd36f34b..ee0439105487cb921267d90ac6ab9f38385b62b3 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,4 +1,4 @@ -%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button +%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button %i.fa.fa-paperclip %span Choose File ... diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 3b82d8e686f37d7075be2a475368ec1b652e7c4a..03684389742927d404203230d540acd99a57656e 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -2,12 +2,12 @@ .git-clone-holder.input-group .input-group-btn - -if allowed_protocols_present? + - if allowed_protocols_present? .clone-dropdown-btn.btn.btn-static %span = enabled_project_button(project, enabled_protocol) - else - %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', data: { toggle: 'dropdown' }} + %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } } %span = default_clone_protocol.upcase = icon('caret-down') @@ -19,7 +19,7 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn - = clipboard_button(clipboard_target: '#project_clone') + = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard") :javascript $('ul.clone-options-dropdown a').on('click',function(e){ diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 0a38327baa2eb6ca47ba78f655840e18dc6147a9..c196bc06b174e25938382a9c20a4736e0d119ae0 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,5 +1,6 @@ .form-group.commit_message-group - nonce = SecureRandom.hex + - descriptions = local_assigns.slice(:message_with_description, :message_without_description) = label_tag "commit_message-#{nonce}", class: 'control-label' do Commit message .col-sm-10 @@ -8,9 +9,17 @@ = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], + data: descriptions, required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint Try to keep the first line under 52 characters and the others under 72. + - if descriptions.present? + %p.hint.js-with-description-hint + = link_to "#", class: "js-with-description-link" do + Include description in commit message + %p.hint.js-without-description-hint.hide + = link_to "#", class: "js-without-description-link" do + Don't include description in commit message diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index b0fc60573f734abc6424b08b1731ac5b7186deee..e7cb93b17a714c74d0524f6ceedb50eaf0750bb7 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -1,8 +1,8 @@ -#modal-confirm-danger.modal{tabindex: -1} +#modal-confirm-danger.modal{ tabindex: -1 } .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Confirmation required diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index e26693bf5b9613a0419b520b10e983371e6d1e9f..8d64cb5d698b81b1da27c8dd91782f46d6ab1f0f 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -9,8 +9,8 @@ - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. - %a.diff-line-num{href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i} + %a.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i } = link_icon = i - .blob-content{data: {blob_id: blob.id}} + .blob-content{ data: { blob_id: blob.id } } = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?) diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index db324d8868e26284babe9ca0f78cd705ee94ab3d..f11f4471a9d29fdf5ca293add5ec5ee48cf8707b 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -4,7 +4,7 @@ - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] -%li{id: label_css_id, data: { id: label.id } } +%li{ id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown @@ -56,7 +56,7 @@ = icon('spinner spin', class: 'label-subscribe-button-loading') .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span Subscribe = icon('chevron-down') %ul.dropdown-menu diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 73d288e22366f32229b4761c5e2032691b4d81d1..39294fe1a0989d816e05b0a8fc6c4f94e6b71673 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -2,17 +2,17 @@ - counts = milestone_counts(@project.milestones) %ul.nav-links - %li{class: milestone_class_for_state(params[:state], 'opened', true)} + %li{ class: milestone_class_for_state(params[:state], 'opened', true) }> = link_to milestones_filter_path(state: 'opened') do Open - if @project %span.badge #{counts[:opened]} - %li{class: milestone_class_for_state(params[:state], 'closed')} + %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed') do Closed - if @project %span.badge #{counts[:closed]} - %li{class: milestone_class_for_state(params[:state], 'all')} + %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all') do All - if @project diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml index 4e3b1b3a571da49e13ca1edc61a845980bcfcbb2..61646f150c106bb10f13cf76dcbc357c45a58456 100644 --- a/app/views/shared/_nav_scroll.html.haml +++ b/app/views/shared/_nav_scroll.html.haml @@ -1,4 +1,4 @@ .fade-left = icon('angle-left') .fade-right - = icon('angle-right') \ No newline at end of file + = icon('angle-right') diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index a43bf33751a9365520535d0e569e4842528541d6..ed6fc76c61e2d84ac3d13ad749b6c586a7d8e2eb 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? - .no-password-message.alert.alert-warning.hidden-xs + .no-password-message.alert.alert-warning You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put | = link_to 'Remind later', '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index bb5fff2d3bbc1a549dc293620532ddb839eef2ed..d663fa13d10e8cd44d420201e6a0c7999bffb567 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? - .no-ssh-key-message.alert.alert-warning.hidden-xs + .no-ssh-key-message.alert.alert-warning You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index ede3c7090d75c5846f3c9eb37507c8ffbe091115..0ce0d759e86764ea27f79286418646175914636b 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } } %span.light - if @sort.present? = sort_options_hash[@sort] diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 60353aee7f15750eaf4611a954771dc9fda6a6dd..b6047ece5927b66b5ae113a44f4a6b542fa8381d 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,23 +1,23 @@ %ul.nav-links - %li{ class: ('active' if scope.nil?) } + %li{ class: ('active' if scope.nil?) }> = link_to build_path_proc.call(nil) do All %span.badge.js-totalbuilds-count = number_with_delimiter(all_builds.count(:id)) - %li{ class: ('active' if scope == 'pending') } + %li{ class: ('active' if scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending %span.badge = number_with_delimiter(all_builds.pending.count(:id)) - %li{ class: ('active' if scope == 'running') } + %li{ class: ('active' if scope == 'running') }> = link_to build_path_proc.call('running') do Running %span.badge = number_with_delimiter(all_builds.running.count(:id)) - %li{ class: ('active' if scope == 'finished') } + %li{ class: ('active' if scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished %span.badge diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index e939278bc073baa80b509dd78f404eaf5fd1e7d3..e20336540182f0915d67e8d1fd5e8f5200219c8f 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -8,12 +8,12 @@ = render 'shared/empty_states/icons/issues.svg' .col-xs-12{ class: "#{'col-sm-6' if has_button}" } .text-content - - if has_button + - if has_button && current_user %h4 - The Issue Tracker is a good place to add things that need to be improved or solved in a project! + The Issue Tracker is the place to add things that need to be improved or solved in a project %p - An issue can be a bug, a todo or a feature request that needs to be discussed in a project. - Besides, issues are searchable and filterable. + Issues can be bugs, tasks or ideas to be discussed. + Also, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' - else diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 19221e3391fcb16bf3f70ce06d85c8acc70a43b8..f9a7aa4e29bd7372be09fa6d77c76086cbfa630f 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -21,14 +21,14 @@ = icon('users') = number_with_delimiter(group.users.count) - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) } = visibility_level_icon(group.visibility_level, fw: false) .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do - = group.name + = group.full_name - if group_member as diff --git a/app/views/shared/icons/_go_logo.svg.erb b/app/views/shared/icons/_go_logo.svg.erb new file mode 100644 index 0000000000000000000000000000000000000000..5052651c11097647471ae4c6dc3e5d56b3dc5a62 --- /dev/null +++ b/app/views/shared/icons/_go_logo.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"><g fill-rule="evenodd" transform="translate(0 1)"><path d="m14 15.01h1v-8.02c0-3.862-3.134-6.991-7-6.991-3.858 0-7 3.13-7 6.991v8.02h1v-8.02c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02m-10.52-13.354c-.366-.402-.894-.655-1.48-.655-1.105 0-2 .895-2 2 0 .868.552 1.606 1.325 1.883.102-.321.226-.631.371-.93-.403-.129-.695-.507-.695-.953 0-.552.448-1 1-1 .306 0 .58.138.764.354.222-.25.461-.483.717-.699m9.04-.002c.366-.401.893-.653 1.479-.653 1.105 0 2 .895 2 2 0 .867-.552 1.606-1.324 1.883-.101-.321-.225-.632-.37-.931.403-.129.694-.507.694-.952 0-.552-.448-1-1-1-.305 0-.579.137-.762.353-.222-.25-.461-.483-.717-.699"/><path d="m5.726 7.04h1.557v.124c0 .283-.033.534-.1.752-.065.202-.175.391-.33.566-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571-.376-.382-.564-.841-.564-1.377 0-.547.191-1.01.574-1.391.382-.382.848-.574 1.396-.574.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367.387-.381.853-.571 1.396-.571.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379-.389.379-.858.569-1.408.569-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01-.267-.273-.597-.41-.991-.41-.392 0-.723.137-.993.41-.27.27-.405.604-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5 0-.276-.448-.5-1-.5-.552 0-1 .224-1 .5 0 .276.448.5 1 .5"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg old mode 100644 new mode 100755 index 41a210a8ed97241965caf6fa471b0d170923eb51..bd5d04e1cd7c4f378716cc5524d83c361917d995 --- a/app/views/shared/icons/_icon_status_canceled.svg +++ b/app/views/shared/icons/_icon_status_canceled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..bf7fb29185f0b38294da89ba2a05549b495f122e --- /dev/null +++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg old mode 100644 new mode 100755 index 1f5c3b51b0386aaf4fecf79893bf38714b37169b..326ad04e017005045e1f9ea5cb8cc4d3397538cc --- a/app/views/shared/icons/_icon_status_created.svg +++ b/app/views/shared/icons/_icon_status_created.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..1810d023be8c7c9c825975eaf5241858fd1d7951 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg> diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg old mode 100644 new mode 100755 index af267b8938a3132658a538ad69bc002861a9109f..64da5aa31fc612d52c4fba5858c04ec150588ecd --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..b7022350c74cd0668a1f360e64b8eefa54ad19cd --- /dev/null +++ b/app/views/shared/icons/_icon_status_failed_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg> diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg new file mode 100755 index 0000000000000000000000000000000000000000..c98839f51a933196085aef6dc364858ca8dc0825 --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..5eec665688b127eda090bf7bbce96f81a8a1dd9a --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg> diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg old mode 100644 new mode 100755 index 516231d1b4487dc60b0fcefe9ec98a2470b9b013..02d5da407e3068867bd8f64326e5da964f99e36f --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..8d66e9e6c9c71dc4e58e50c84d9427f06fce8d81 --- /dev/null +++ b/app/views/shared/icons/_icon_status_pending_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg old mode 100644 new mode 100755 index d2618bce200bc0dd8a236e1e708e8b440bc1bf4f..532f4fee33ce8a6a2faa802472fe8044c6d2a544 --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..2757a168ed5e344bd6f3572e617fdcec8fa3265b --- /dev/null +++ b/app/views/shared/icons/_icon_status_running_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg old mode 100644 new mode 100755 index 701f33bcbea00db7942c99c720173605dbf00e01..1998dfef9ea0ae10aefe7a672e7a4ab553f538fa --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ -<svg width="14" height="14" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..fb3e930b3cb5184d6834b06e8cad7f9a608ad402 --- /dev/null +++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg old mode 100644 new mode 100755 index b7c21ba6971a447d10ef1d9492f08d007b668fa1..eed5006bebeae4539119736e0e0218d25f233a94 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..8ee5be7ab78dda4d8fcf8ea175827a9455f0fd2a --- /dev/null +++ b/app/views/shared/icons/_icon_status_success_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg> diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg old mode 100644 new mode 100755 index 9191e0050a644f0cc96223b78d4e15c18a6a4055..cb785635b7edd19ec7da1203a64a9081be5afa97 --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg new file mode 100644 index 0000000000000000000000000000000000000000..7b061624521d38d0153b3a74f23045e00cf60864 --- /dev/null +++ b/app/views/shared/icons/_icon_status_warning_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg new file mode 100644 index 0000000000000000000000000000000000000000..f20de04538e8238851798d321b1a8b5e9a20b774 --- /dev/null +++ b/app/views/shared/icons/_icon_stopwatch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg> \ No newline at end of file diff --git a/app/views/shared/icons/_icon_terminal.svg b/app/views/shared/icons/_icon_terminal.svg new file mode 100644 index 0000000000000000000000000000000000000000..c80f44c3edfceac3d487d45cdf60be2076ad4150 --- /dev/null +++ b/app/views/shared/icons/_icon_terminal.svg @@ -0,0 +1 @@ +<svg width="19" height="14" viewBox="0 0 19 14" xmlns="http://www.w3.org/2000/svg"><rect fill="#848484" x="7.2" y="9.25" width="6.46" height="1.5" rx=".5"/><path d="M5.851 7.016L3.81 9.103a.503.503 0 0 0 .017.709l.35.334c.207.198.524.191.717-.006l2.687-2.748a.493.493 0 0 0 .137-.376.493.493 0 0 0-.137-.376L4.894 3.892a.507.507 0 0 0-.717-.006l-.35.334a.503.503 0 0 0-.017.709L5.85 7.016z"/><path d="M1.25 11.497c0 .691.562 1.253 1.253 1.253h13.994c.694 0 1.253-.56 1.253-1.253V2.503c0-.691-.562-1.253-1.253-1.253H2.503c-.694 0-1.253.56-1.253 1.253v8.994zM2.503 0h13.994A2.504 2.504 0 0 1 19 2.503v8.994A2.501 2.501 0 0 1 16.497 14H2.503A2.504 2.504 0 0 1 0 11.497V2.503A2.501 2.501 0 0 1 2.503 0z"/></svg> diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb new file mode 100644 index 0000000000000000000000000000000000000000..83fbd1a407de605d84f505f11dea5fcf97c954a1 --- /dev/null +++ b/app/views/shared/icons/_mattermost_logo.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg> diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg new file mode 100644 index 0000000000000000000000000000000000000000..acf22ac9314f19207ff6a66f2305f1a1db27451e --- /dev/null +++ b/app/views/shared/icons/_scroll_down.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg new file mode 100644 index 0000000000000000000000000000000000000000..262576acf54249fe585a74e9c79d41537253efe6 --- /dev/null +++ b/app/views/shared/icons/_scroll_down_hover_active.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg new file mode 100644 index 0000000000000000000000000000000000000000..f11288fd59c1c9f8d667b3b9bf3f308a68cdcd3d --- /dev/null +++ b/app/views/shared/icons/_scroll_up.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg new file mode 100644 index 0000000000000000000000000000000000000000..4658dbb1bb7ff9a637c13fdd904325307a992805 --- /dev/null +++ b/app/views/shared/icons/_scroll_up_hover_active.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index e3503981afe220dd3fb559178217553f36d37f5f..b42eaabb111b220dc928b32261182fcc12ae9b56 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -31,11 +31,11 @@ - if issuable_filter_present? .filter-item.inline.reset-filters - %a{href: page_filter_path(without: issuable_filter_params)} Reset filters + %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters .pull-right - if boards_page - #js-boards-seach.issue-boards-search + #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) .dropdown.pull-right @@ -56,9 +56,9 @@ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li - %a{href: "#", data: {id: "reopen"}} Open + %a{ href: "#", data: { id: "reopen" } } Open %li - %a{href: "#", data: {id: "close"}} Closed + %a{ href: "#", data: {id: "close" } } Closed .filter-item.inline = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) @@ -70,9 +70,9 @@ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul %li - %a{href: "#", data: {id: "subscribe"}} Subscribe + %a{ href: "#", data: { id: "subscribe" } } Subscribe %li - %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index bdb00bfa33c6148bd932cb38240b900702cacbe1..dcc1f3ba676da82d2012120b67960e5cb8dffe67 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -58,7 +58,7 @@ as resolved. Ask someone with sufficient rights to resolve the them. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) -.row-content-block{class: (is_footer ? "footer-block" : "middle-block")} +.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } - if issuable.new_record? = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 22b5a6aa11bebb39a9f273de03656dc93564fea7..93c7fa0c7d65d29d32048c4002547a0b60cd14d3 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -19,7 +19,7 @@ = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data } %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } = multi_label_name(selected, "Labels") = icon('chevron-down') diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index a8f01026ca553179512ab4e3581d614b021cdec3..9a8529c6cbb7a90ced227ec5b4c3fd5e50c9b2e8 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -17,7 +17,7 @@ %ul.dropdown-footer-list - if can?(current_user, :admin_label, @project) %li - %a.dropdown-toggle-page{href: "#"} + %a.dropdown-toggle-page{ href: "#" } Create new label %li = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 40fe53e6a8daca6facd60ed80bd670ebd5d9ed60..415361f8fbf0ba453cf1363a5f03763a5a3d382c 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -3,7 +3,7 @@ - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") -- if selected.present? +- if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 0af92b595848f7606900243c25ed64bc7cd78d47..1154316c03f25a47741c71251ac5d098a8c9331f 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -3,23 +3,23 @@ - issuables = @issues || @merge_requests %ul.nav-links.issues-state-filters - %li{class: ("active" if params[:state] == 'opened')} + %li{ class: ("active" if params[:state] == 'opened') }> = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests - %li{class: ("active" if params[:state] == 'merged')} + %li{ class: ("active" if params[:state] == 'merged') }> = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} - %li{class: ("active" if params[:state] == 'closed')} + %li{ class: ("active" if params[:state] == 'closed') }> = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else - %li{class: ("active" if params[:state] == 'closed')} + %li{ class: ("active" if params[:state] == 'closed') }> = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} - %li{class: ("active" if params[:state] == 'all')} + %li{ class: ("active" if params[:state] == 'all') }> = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml index 33a9a49485797daa07c483bb2173bbea72fc53cb..171da89993736f94196b1e57a65066ef354e3a11 100644 --- a/app/views/shared/issuable/_participants.html.haml +++ b/app/views/shared/issuable/_participants.html.haml @@ -13,8 +13,8 @@ .participants-author.js-participants-author = link_to_member(@project, participant, name: false, size: 24) - if participants_extra > 0 - %div.participants-more - %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}} + .participants-more + %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } } + #{participants_extra} more :javascript IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row}; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..8d7b1d616f476b21f13cf2a807ea783943dd4d2b --- /dev/null +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -0,0 +1,127 @@ +- type = local_assigns.fetch(:type) + +.issues-filters + .issues-details-filters.row-content-block.second-block.filtered-search-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @bulk_edit + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left" + .issues-other-filters.filtered-search-container + .filtered-search-input-container + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id } + = icon('filter') + %button.clear-search.hidden{ type: 'button' } + = icon('times') + #js-dropdown-hint.dropdown-menu.hint-dropdown + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => '' } + %button.btn.btn-link + = icon('search') + %span + Keep typing and press Enter + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %i.fa{ class: "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-author.dropdown-menu + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-assignee.dropdown-menu + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Assignee + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Milestone + %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %button.btn.btn-link + Upcoming + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value + {{title}} + #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Label + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link + %span.dropdown-label-box{ style: 'background: {{color}}' } + %span.label-title.js-data-value + {{title}} + .pull-right + = render 'shared/sort_dropdown' + + - if @bulk_edit + .issues_bulk_update.hide + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do + .filter-item.inline + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + %ul + %li + %a{ href: "#", data: { id: "reopen" } } Open + %li + %a{ href: "#", data: { id: "close" } } Closed + .filter-item.inline + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + .filter-item.inline + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + .filter-item.inline + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + %ul + %li + %a{ href: "#", data: { id: "subscribe" } } Subscribe + %li + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe + + = hidden_field_tag 'update[issuable_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + .filter-item.inline + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" + +:javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 958f8413e1d7fcc4e633cd8f39054c9122e334cb..ec9bcaf63ddf992e4e2f639c8c8e63aa98bdb567 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,7 @@ - todo = issuable_todo(issuable) -%aside.right-sidebar{ class: sidebar_gutter_collapsed_class } +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('issuable/issuable_bundle.js') +%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header @@ -17,9 +19,9 @@ Add todo = icon('spin spinner', class: 'hidden js-issuable-todo-loading') - = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + = 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.name 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 @@ -54,7 +56,7 @@ = icon('clock-o') %span - if issuable.milestone - %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}} + %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } = issuable.milestone.title - else None @@ -72,7 +74,13 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - + - if issuable.has_attribute?(:time_estimate) + #issuable-time-tracker.block + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') } + // Fallback while content is loading + .title.hide-collapsed + Time tracking + = icon('spinner spin') - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon @@ -97,7 +105,7 @@ remove due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .selectbox.hide-collapsed - = f.hidden_field :due_date, value: issuable.due_date + = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd') .dropdown %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } %span.dropdown-toggle-text Due date @@ -129,8 +137,8 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} - %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)} + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } + %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable @@ -141,7 +149,7 @@ = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user - subscribed = issuable.subscribed?(current_user, @project) - .block.light.subscription{data: {url: toggle_subscription_path(issuable)}} + .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } } .sidebar-collapsed-icon = icon('rss') %span.issuable-header-text.hide-collapsed.pull-left @@ -153,18 +161,20 @@ - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") .cross-project-reference.hide-collapsed %span Reference: - %cite{title: project_ref} + %cite{ title: project_ref } = project_ref - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") :javascript + gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); + new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); gl.Subscription.bindAll('.subscription'); new gl.DueDateSelectors(); - sidebar = new Sidebar(); + window.sidebar = new Sidebar(); diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index e166dfab710f82ff8104aee110bdf52ab6af682d..fb795ad1c721e616ea82d49c7eff3f5b0ae8fc20 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,16 +1,17 @@ - model_name = source.model_name.to_s.downcase -- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) - = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: leave_confirmation_message(source) }, - class: 'btn' -- elsif requester = source.requesters.find_by(user_id: current_user.id) - = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: remove_member_message(requester) }, - class: 'btn' -- elsif source.request_access_enabled && can?(current_user, :request_access, source) - = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), - method: :post, - class: 'btn' +.project-action-button.inline + - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) + = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(source) }, + class: 'btn' + - elsif requester = source.requesters.find_by(user_id: current_user.id) + = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: remove_member_message(requester) }, + class: 'btn' + - elsif source.request_access_enabled && can?(current_user, :request_access, source) + = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), + method: :post, + class: 'btn' diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 8928de9097b1465fb4f7e920cbe538c17203a999..81b5bc1de300c2ccfc8767f83afcafe72a7e2089 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -3,7 +3,7 @@ - can_admin_member = can?(current_user, :admin_project_member, @project) - dom_id = "group_member_#{group_link.id}" %li.member.group_member{ id: dom_id } - %span{ class: "list-item-name" } + %span.list-item-name = image_tag group_icon(group), class: "avatar s40", alt: '' %strong = link_to group.name, group_path(group) @@ -37,7 +37,6 @@ %i.clear-icon.js-clear-input - if can_admin_member = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), - remote: true, method: :delete, data: { confirm: "Are you sure you want to remove #{group.name}?" }, class: 'btn btn-remove prepend-left-10' do diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..bad0891f9f21102a218c36566d61f99f22736bf3 --- /dev/null +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -0,0 +1,9 @@ +.dropdown.inline.member-sort-dropdown + = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Sort by + - member_sort_options_hash.each do |value, title| + %li + = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do + = title diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 9e1b03794285f2436dffe00e5818238026412ce0..51c195ffbcd057fd3e2850685469b6dab7da8336 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -14,15 +14,15 @@ - if issuable.is_a?(Issue) = confidential_icon(issuable) = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title - %div{class: 'issuable-detail'} + .issuable-detail = link_to [project.namespace.becomes(Namespace), project, issuable] do - %span{ class: 'issuable-number' }>= issuable.to_reference + %span.issuable-number >= issuable.to_reference - issuable.labels.each do |label| = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do - render_colored_label(label) - %span{ class: "assignee-icon" } + %span.assignee-icon - if assignee = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 8619939dde7993c575825c8d22704f13793d4bf3..c8fd45c431998558683b744d2d18b27deee3ad74 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -3,10 +3,13 @@ - panel_class = primary ? 'panel-primary' : 'panel-default' .panel{ class: panel_class } - .panel-heading - = title + .panel-heading.split + .left + = title - if show_counter - .pull-right= issuables.size + .right + = issuables.size + .pull-right= number_with_delimiter(issuables.size) - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 3dccfb147bfaab5363c8842147ca6df4716eaa5f..9e6a76e1ddb1e1898fb17224ef8e7207f94ceecf 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -1,7 +1,7 @@ - dashboard = local_assigns[:dashboard] - custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first) -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } +%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } .row .col-sm-6 %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path @@ -9,7 +9,7 @@ .pull-right.light #{milestone.percent_complete(current_user)}% complete .row .col-sm-6 - = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path + = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 2b6ce2d7e7a38cf6304802b540d5f8dbb32745c1..c8f2319d95aa95afd3a9148893f82789688278ac 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -21,7 +21,7 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index fbad0d05de308a9402f4df27e9b4150eb13eaa72..1d072c16b32ee0cc72412912712665e8fb2f41c5 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,5 +1,5 @@ - if notification_setting - .dropdown.notification-dropdown + .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = hidden_setting_source_input(notification_setting) = f.hidden_field :level, class: "notification_setting_level" diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index a82fc95df84a92582f1cc1afb7549d5048c66ec5..b5c0a7fd6d4e4a32e390b56ed4042a89821febbf 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -18,7 +18,7 @@ %p Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out = succeed "." do - %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails + %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank" } notification emails .col-lg-8 - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 264391fe84f6fd25403e7e9952a61e9e882cb7fc..4a27965754d137859cc98accbd04a45f7e566336 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -25,7 +25,7 @@ %span = icon('star') = number_with_delimiter(project.star_count) - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} + %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } = visibility_level_icon(project.visibility_level, fw: true) .title diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index dcdba01aee9f4a12b9bf220cc802717e411082d9..ad5c0c2d8c80f6662aaffa25b50352154198ac62 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,6 +1,6 @@ - unless @snippet.content.empty? - if markup?(@snippet.file_name) - %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}} + %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } } = @snippet.content .file-content.wiki - if gitlab_markdown?(@snippet.file_name) diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..5074afb63a1245bdd4a18fc48a80cf0542f09a3b --- /dev/null +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -0,0 +1,9 @@ +- scopes = local_assigns.fetch(:scopes) +- prefix = local_assigns.fetch(:prefix) +- token = local_assigns.fetch(:token) + +- scopes.each do |scope| + %fieldset + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" + = label_tag "#{prefix}_scopes_#{scope}", scope + %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..f99e905e95c7b261ca7adbcb4b7a93cf6b016cca --- /dev/null +++ b/app/views/shared/tokens/_scopes_list.html.haml @@ -0,0 +1,13 @@ +- token = local_assigns.fetch(:token) + +- return unless token.scopes.present? + +%tr + %td + Scopes + %td + %ul.scopes-list.append-bottom-0 + - token.scopes.each do |scope| + %li + %span.scope-name= scope + = "(#{t(scope, scope: [:doorkeeper, :scopes])})" diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml index 94d4dd4fa7d78f277c2fe75666e399f35ef8b400..92151176fce2a0c5d24a05ca1e065eb229092b8d 100644 --- a/app/views/sherlock/file_samples/show.html.haml +++ b/app/views/sherlock/file_samples/show.html.haml @@ -41,7 +41,7 @@ %th= t('sherlock.percent') %tbody - @file_sample.line_samples.each_with_index do |sample, index| - %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''} + %tr{ class: sample.majority_of?(@file_sample.duration) ? 'slow' : '' } %td= index + 1 %td= sample.events %td diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 7073c0f4d90aad9ef931fdc7ad1196badd16e31e..5a447f791dc6aa7ce164b65a86c74bb5696077c9 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -26,7 +26,7 @@ .panel.panel-default .panel-heading .pull-right - %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button } %i.fa.fa-clipboard %pre.hidden = @query.formatted_query @@ -41,7 +41,7 @@ .panel.panel-default .panel-heading .pull-right - %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button } %i.fa.fa-clipboard %pre.hidden = @query.explain diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml index fc2863dca8e065db325d55c2909b924d73f5ff64..c45da6ee9a4084a901bedc3f1c90ea676934b959 100644 --- a/app/views/sherlock/queries/show.html.haml +++ b/app/views/sherlock/queries/show.html.haml @@ -3,10 +3,10 @@ %ul.nav-links %li.active - %a(href="#tab-general" data-toggle="tab") + %a{ href: "#tab-general", data: { toggle: "tab" } } = t('sherlock.general') %li - %a(href="#tab-backtrace" data-toggle="tab") + %a{ href: "#tab-backtrace", data: { toggle: "tab" } } = t('sherlock.backtrace') .row-content-block diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml index da969c0276523275b7c8457ee65f7596a6eb809d..bc05659dfa831a2f558f5db7874875699e2e7f95 100644 --- a/app/views/sherlock/transactions/index.html.haml +++ b/app/views/sherlock/transactions/index.html.haml @@ -28,7 +28,7 @@ %tr %td= trans.type %td - %span{title: trans.path} + %span{ title: trans.path } = truncate(trans.path, length: 70) %td = trans.duration.round(2) diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index 8aa6b437d953263757e6e0cd7450538ecbb25d73..eab91e8fbe4ce86a5e5acbc4a485289ffedf5607 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -3,15 +3,15 @@ %ul.nav-links %li.active - %a(href="#tab-general" data-toggle="tab") + %a{ href: "#tab-general", data: { toggle: "tab" } } = t('sherlock.general') %li - %a(href="#tab-queries" data-toggle="tab") + %a{ href: "#tab-queries", data: { toggle: "tab" } } = t('sherlock.queries') %span.badge #{@transaction.queries.length} %li - %a(href="#tab-file-samples" data-toggle="tab") + %a{ href: "#tab-file-samples", data: { toggle: "tab" } } = t('sherlock.file_samples') %span.badge #{@transaction.file_samples.length} diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 27d7a6c5bb67a202c55fa1f49304edaa6ef211da..837a1a0cc8c9d0c2a7605a76703bd68095dc6cfe 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,9 +7,9 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" = render 'shared/snippets/blob' -= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file += render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 232ca26c1af0a60faf0e2f99bf7d5a8300315736..f878bece2fac3c788e25d16215945d8a3837a82b 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -1,30 +1,21 @@ #js-authenticate-u2f +%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code %script#js-authenticate-u2f-not-supported{ type: "text/template" } %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). -%script#js-authenticate-u2f-setup{ type: "text/template" } - %div - %p Insert your security key (if you haven't already), and press the button below. - %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device - %script#js-authenticate-u2f-in-progress{ type: "text/template" } %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. %script#js-authenticate-u2f-error{ type: "text/template" } %div - %p <%= error_message %> - %a.btn.btn-warning#js-u2f-try-again Try again? + %p <%= error_message %> (error code: <%= error_code %>) + %a.btn.btn-block.btn-warning#js-u2f-try-again Try again? %script#js-authenticate-u2f-authenticated{ type: "text/template" } %div - %p We heard back from your U2F device. Click this button to authenticate with the GitLab server. - = form_tag(new_user_session_path, method: :post) do |f| + %p We heard back from your U2F device. You have been authenticated. + = form_tag(new_user_session_path, method: :post, id: 'js-login-u2f-form') do |f| - resource_params = params[resource_name].presence || params = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0) = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" - = submit_tag "Authenticate via U2F Device", class: "btn btn-success" - -:javascript - var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f); - u2fAuthenticate.start(); diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 8f7b42eb351f4e34aee018029474bac896dc1792..adc07bcba733187541618b726e6e001f580af2a2 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -23,11 +23,11 @@ %script#js-register-u2f-error{ type: "text/template" } %div %p - %span <%= error_message %> + %span <%= error_message %> (error code: <%= error_code %>) %a.btn.btn-warning#js-u2f-try-again Try again? %script#js-register-u2f-registered{ type: "text/template" } - %div.row.append-bottom-10 + .row.append-bottom-10 .col-md-12 %p Your device was successfully set up! Give it a name and register it with the GitLab server. = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 09ff8a76d2711da184e8b7e7b10bf74064607039..6228245d8d0fc0f368bbca728e211392f82d67ae 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -6,4 +6,4 @@ new Calendar( #{@activity_dates.to_json}, '#{user_calendar_activities_path}' - ); \ No newline at end of file + ); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 1e0752bd3c3925abb7f9669872cded2a9e66017f..fb25eed4f37e612ef7ed60e5da7de901f08dd2fa 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -18,11 +18,11 @@ - elsif current_user - if @user.abuse_report %button.btn.btn-danger{ title: 'Already reported for abuse', - data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }} + data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } } = icon('exclamation-circle') - else = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', - title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - if current_user = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do @@ -101,12 +101,12 @@ .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - .user-calendar{data: {href: user_calendar_path}} + .user-calendar{ data: { href: user_calendar_path } } %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities - .content_list{ data: {href: user_path} } + .content_list{ data: { href: user_path } } = spinner #groups.tab-pane diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index fccddb70d189d62d4a84d686d2bca54a008c355a..2badd0680fbaae34867aeee28149e81ab8d49deb 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -2,8 +2,6 @@ class AuthorizedProjectsWorker include Sidekiq::Worker include DedicatedSidekiqQueue - LEASE_TIMEOUT = 1.minute.to_i - def self.bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end @@ -11,24 +9,6 @@ class AuthorizedProjectsWorker def perform(user_id) user = User.find_by(id: user_id) - refresh(user) if user - end - - def refresh(user) - lease_key = "refresh_authorized_projects:#{user.id}" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - - until uuid = lease.try_obtain - # Keep trying until we obtain the lease. If we don't do so we may end up - # not updating the list of authorized projects properly. To prevent - # hammering Redis too much we'll wait for a bit between retries. - sleep(1) - end - - begin - user.refresh_authorized_projects - ensure - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - end + user.refresh_authorized_projects if user end end diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 27d7e6527210cc503e4ce68a42b15ea4bb3ed91b..8ff9d07860fdf0ab9eaf7ca85cdca43a0e9b6749 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -6,26 +6,27 @@ class ProjectCacheWorker LEASE_TIMEOUT = 15.minutes.to_i # project_id - The ID of the project for which to flush the cache. - # refresh - An Array containing extra types of data to refresh such as - # `:readme` to flush the README and `:changelog` to flush the - # CHANGELOG. - def perform(project_id, refresh = []) + # files - An Array containing extra types of files to refresh such as + # `:readme` to flush the README and `:changelog` to flush the + # CHANGELOG. + # statistics - An Array containing columns from ProjectStatistics to + # refresh, if empty all columns will be refreshed + def perform(project_id, files = [], statistics = []) project = Project.find_by(id: project_id) return unless project && project.repository.exists? - update_repository_size(project) - project.update_commit_count + update_statistics(project, statistics.map(&:to_sym)) - project.repository.refresh_method_caches(refresh.map(&:to_sym)) + project.repository.refresh_method_caches(files.map(&:to_sym)) end - def update_repository_size(project) - return unless try_obtain_lease_for(project.id, :update_repository_size) + def update_statistics(project, statistics = []) + return unless try_obtain_lease_for(project.id, :update_statistics) - Rails.logger.info("Updating repository size for project #{project.id}") + Rails.logger.info("Updating statistics for project #{project.id}") - project.update_repository_size + project.statistics.refresh!(only: statistics) end private diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..18b8daf4e1e7571e1a786e285b4224a74d7cf7fc --- /dev/null +++ b/app/workers/reactive_caching_worker.rb @@ -0,0 +1,15 @@ +class ReactiveCachingWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(class_name, id, *args) + klass = begin + Kernel.const_get(class_name) + rescue NameError + nil + end + return unless klass + + klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args) + end +end diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..c9d382cc5d6e0b03a93bdcb0cc49b9836a077db4 --- /dev/null +++ b/app/workers/use_key_worker.rb @@ -0,0 +1,13 @@ +class UseKeyWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(key_id) + key = Key.find(key_id) + key.touch(:last_used_at) + rescue ActiveRecord::RecordNotFound + Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job") + + false + end +end diff --git a/bin/changelog b/bin/changelog index e07b1ad237ad8f4e2f968f9aaf0ded5971d8c517..4c894f8ff5bfaba3866554d92f74aa7d853f8a1c 100755 --- a/bin/changelog +++ b/bin/changelog @@ -84,12 +84,15 @@ class ChangelogEntry end end + private + def contents - YAML.dump( + yaml_content = YAML.dump( 'title' => title, 'merge_request' => options.merge_request, 'author' => options.author ) + remove_trailing_whitespace(yaml_content) end def write @@ -101,8 +104,6 @@ class ChangelogEntry exec("git commit --amend") end - private - def fail_with(message) $stderr.puts "\e[31merror\e[0m #{message}" exit 1 @@ -160,6 +161,10 @@ class ChangelogEntry def branch_name @branch_name ||= %x{git symbolic-ref --short HEAD}.strip end + + def remove_trailing_whitespace(yaml_content) + yaml_content.gsub(/ +$/, '') + end end if $0 == __FILE__ diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml deleted file mode 100644 index 19c76b5b4372b08ae312d0a528f79b217d883a5e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix wrong tab selected when loggin fails and multiple login tabs exists -merge_request: 7314 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml deleted file mode 100644 index c76e17340f24080220a1b5e94a6320ec67f6f7d6..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/18546-update-wiki-page-design.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update wiki page design -merge_request: 7429 -author: diff --git a/changelogs/unreleased/18786-go-to-a-project-order.yml b/changelogs/unreleased/18786-go-to-a-project-order.yml new file mode 100644 index 0000000000000000000000000000000000000000..1b9e246d1a77ff3d35c4de392673638d73a44647 --- /dev/null +++ b/changelogs/unreleased/18786-go-to-a-project-order.yml @@ -0,0 +1,4 @@ +--- +title: Go to a project order +merge_request: 7737 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/19086-double-newline.yml b/changelogs/unreleased/19086-double-newline.yml new file mode 100644 index 0000000000000000000000000000000000000000..dd9b58920fb23f2d058ada50575834e1c7e468ff --- /dev/null +++ b/changelogs/unreleased/19086-double-newline.yml @@ -0,0 +1,4 @@ +--- +title: Fix double spaced CI log +merge_request: 8349 +author: Jared Deckard <jared.deckard@gmail.com> diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml deleted file mode 100644 index 742b10e72aa67152e3daae28eb4620b48fdc8072..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: group authors in contribution graph with case insensitive email handle comparison -merge_request: 8021 -author: diff --git a/changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml b/changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml new file mode 100644 index 0000000000000000000000000000000000000000..61cf047026b993d61a023101a8b5d6097a58f0a6 --- /dev/null +++ b/changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml @@ -0,0 +1,4 @@ +--- +title: Allow group and project paths when transferring projects via the API +merge_request: +author: diff --git a/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml b/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml new file mode 100644 index 0000000000000000000000000000000000000000..5570ede4a9a00b0e9a1254fd65a79ce6926c3be4 --- /dev/null +++ b/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml @@ -0,0 +1,4 @@ +--- +title: Prevent empty pagination when list is not empty +merge_request: 8172 +author: diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml deleted file mode 100644 index 779cd08de0968c327a087e57a4bd8eec51adac6a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/20052-actions-table-vscroll.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent overflow with vertical scroll when we have space to show content -merge_request: 8061 -author: diff --git a/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml b/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml new file mode 100644 index 0000000000000000000000000000000000000000..574c322803c5f4a52f598268d332f9b1b81f1e29 --- /dev/null +++ b/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml @@ -0,0 +1,4 @@ +--- +title: Improve visibility of "Resolve conflicts" and "Merge locally" actions +merge_request: 8229 +author: diff --git a/changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml b/changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml new file mode 100644 index 0000000000000000000000000000000000000000..e4f7c1b77621cb62adb1702cd9b6ec932cac67b1 --- /dev/null +++ b/changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml @@ -0,0 +1,4 @@ +--- +title: Remove Lock Icon on Protected Tag +merge_request: 8513 +author: Sergey Nikitin diff --git a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml deleted file mode 100644 index 7f1d40e7c21f32e61454ae4049ee4686baddd30b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Make API::Helpers find a project with only one query' -merge_request: 7714 -author: diff --git a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml deleted file mode 100644 index 54bd313f075abc5d5d8eed7c7d184e2a5e0eec5b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: New `gitlab:workhorse:install` rake task -merge_request: 6574 -author: diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml deleted file mode 100644 index e46739e48e371c8d6bfa78d549cc849a52898eeb..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/22781-user-generated-permalinks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent DOM ID collisions resulting from user-generated content anchors -merge_request: 7631 -author: diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml deleted file mode 100644 index 99dbe4a32a03fe0f6aaf3daec20ede86a3de45da..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Moved Leave Project and Leave Group buttons to access_request_buttons from - the settings dropdown -merge_request: 7600 -author: diff --git a/changelogs/unreleased/23500-enable-colorvariable.yml b/changelogs/unreleased/23500-enable-colorvariable.yml deleted file mode 100644 index 98e22a934b855acb754a6d9745268c282918f3b8..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/23500-enable-colorvariable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable ColorVariable in scss-lint -merge_request: -author: Sam Rose diff --git a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml deleted file mode 100644 index bb9e96d7581e14db73134ee5fa658a3f522f544c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Define common helper for describe pagination params in api -merge_request: 7646 -author: Semyon Pupkov diff --git a/changelogs/unreleased/23589-open-issue-for-mr.yml b/changelogs/unreleased/23589-open-issue-for-mr.yml deleted file mode 100644 index cea48b85254d236ecbb8947454564fbb53080757..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/23589-open-issue-for-mr.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve all discussions in a merge request by creating an issue collecting - them -merge_request: 7180 -author: Bob Van Landuyt diff --git a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml deleted file mode 100644 index 2e7583244ac52aa2f2241137d3b6fea92267d67c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add Human Readable format for rake backup -merge_request: 7188 -author: David GerÅ‘ diff --git a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml deleted file mode 100644 index 855e4e1ba1dc57de30c6f723ad38ff43b6f85b67..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Moved new projects button below new group button on the welcome screen -merge_request: 7770 -author: diff --git a/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml new file mode 100644 index 0000000000000000000000000000000000000000..83cf3670ec046f3081b7a9673735b3999f0ed93a --- /dev/null +++ b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml @@ -0,0 +1,4 @@ +--- +title: Treat environments matching `production/*` as Production +merge_request: 8500 +author: diff --git a/changelogs/unreleased/24150-consistent-dropdown-styles.yml b/changelogs/unreleased/24150-consistent-dropdown-styles.yml deleted file mode 100644 index a328d796c43bbef1486200cabaf351b9c748fe26..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24150-consistent-dropdown-styles.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Homogenize filter and sort dropdown look'n'feel -merge_request: 7583 -author: David Wagner diff --git a/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml b/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml new file mode 100644 index 0000000000000000000000000000000000000000..09ff63a44fb34e4e99dfaf95d74a82f1c843c5de --- /dev/null +++ b/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml @@ -0,0 +1,4 @@ +--- +title: Query external CI statuses in the background +merge_request: +author: diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml deleted file mode 100644 index 2227c81bd34e0ab2b1004e31e83f054f69c093ec..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove the help text under the sidebar subscribe button and style it inline -merge_request: 7389 -author: diff --git a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml deleted file mode 100644 index 972eaed95e0670f8ee3fa3c2d2bb00845255fd7b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Shows unconfirmed email status in profile -merge_request: 7611 -author: diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml deleted file mode 100644 index 34999480d4a0d7baff3c2b8c14d5767c9cad5214..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Remove unnecessary target branch link from MR page in case of deleted target branch' -merge_request: 7916 -author: Rydkin Maxim diff --git a/changelogs/unreleased/24576_cant_stop_impersonating.yml b/changelogs/unreleased/24576_cant_stop_impersonating.yml deleted file mode 100644 index 8fa6eeca756c50ce6210cb7192ba041297968551..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24576_cant_stop_impersonating.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow admins to stop impersonating users without e-mail addresses -merge_request: 7550 -author: Oren Kanner diff --git a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml deleted file mode 100644 index 07cb53d5278e357a1118714325ca43c4f532ec02..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update generic/external build status to match normal build status template -merge_request: 7811 -author: diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml deleted file mode 100644 index 6436e4b688f415e04e57ebefeec900079e03b4a9..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24726-remove-across-gitlab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 24726 Remove Across GitLab from side navigation -merge_request: -author: diff --git a/changelogs/unreleased/24733-archived-project-merge-request-count.yml b/changelogs/unreleased/24733-archived-project-merge-request-count.yml deleted file mode 100644 index 2bc7e91825acab27bec282da600d31bddea2914e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24733-archived-project-merge-request-count.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Archived project merge requests add to group's Merge Requests -merge_request: 7790 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml deleted file mode 100644 index 49e6c5e56e598741d478a98120f56c88547c0c9c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use SmartInterval for MR widget and improve visibilitychange functionality -merge_request: 7762 -author: diff --git a/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml b/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml new file mode 100644 index 0000000000000000000000000000000000000000..fac7697ede1728e0373d1288e1f1349219f2affe --- /dev/null +++ b/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml @@ -0,0 +1,4 @@ +--- +title: fix button layout issue on branches page +merge_request: 8074 +author: diff --git a/changelogs/unreleased/24844-environments-date.yml b/changelogs/unreleased/24844-environments-date.yml deleted file mode 100644 index 2bc23d40a68cffd57bf5cec9a28985dc3ba3bd30..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24844-environments-date.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes Environments displaying incorrect date since 8.14 upgrade -merge_request: -author: diff --git a/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml b/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml new file mode 100644 index 0000000000000000000000000000000000000000..c31c89dc4bc8f5f9d44b3c154edd387b65bc285f --- /dev/null +++ b/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml @@ -0,0 +1,4 @@ +--- +title: ensure permalinks scroll to correct position on multiple clicks +merge_request: 8046 +author: diff --git a/changelogs/unreleased/24915_merge_slash_command.yml b/changelogs/unreleased/24915_merge_slash_command.yml new file mode 100644 index 0000000000000000000000000000000000000000..eb8ced8ab0149b17c0b47ced2d36c1ed57d4f80d --- /dev/null +++ b/changelogs/unreleased/24915_merge_slash_command.yml @@ -0,0 +1,4 @@ +--- +title: Support slash comand `/merge` for merging merge requests. +merge_request: 7746 +author: Jarka Kadlecova diff --git a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml deleted file mode 100644 index 4d4019e770eb77cc057960f901a767f10e6c3f24..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't display prompt to add SSH keys if SSH protocol is disabled -merge_request: 7840 -author: Andrew Smith (EspadaV8) diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml deleted file mode 100644 index b8ba9391530beaa0fccfaba04faa438a87e04a55..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add image controls to MR diffs -merge_request: 7919 -author: diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml deleted file mode 100644 index 12ea08e38157e65bc5af7f69858e5581ca0c362d..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'fix: 24982- Remove''Signed in successfully'' message After this change the - sign-in-success flash message will not be shown' -merge_request: 7837 -author: jnoortheen diff --git a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml deleted file mode 100644 index 7af812e73595c81c526fab2cbf0f27ca0d0c3ec4..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adjust the width of project avatars to fix alignment within their container -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml deleted file mode 100644 index cc8b0e28277a661786704d54515944bcb236511f..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sentence cased the nav tab headers on the project dashboard page -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml deleted file mode 100644 index 2c3ba1dfe44764b443e9490a7ea6e8073ee4fc5a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds hoverstates for collapsed Issue/Merge Request sidebar -merge_request: !7777 -author: diff --git a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml deleted file mode 100644 index a7b5810f1bfdf5fdcb855d13234ff16853d90295..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redirect to sign-in page when unauthenticated user tries to create a snippet -merge_request: 7786 -author: diff --git a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml deleted file mode 100644 index 862de7c5db17126ae858207af34c2cdff25ca485..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not raise error in AutocompleteController#users when not authorized -merge_request: 7817 -author: Semyon Pupkov diff --git a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml deleted file mode 100644 index 1799fad1631c6444e57e06453b2aad43f8a03810..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjusted margins for Build Status and Coverage Report rows to match those of - the CI/CD Pipeline row -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml deleted file mode 100644 index 62030d3fc455f85b16a06ac82988171a0550c4df..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent user creating issue or MR without signing in for a group -merge_request: 7902 -author: diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml deleted file mode 100644 index eab1534aa6688a8df38ed43da1630a3a47ae3748..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25136-last-deployment-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Latest deployment link is broken -merge_request: 7839 -author: diff --git a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml deleted file mode 100644 index a7576e2cbdbfa75636051e1597dfd4df8a784a22..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove wrong '.builds-feature' class from the MR settings fieldset -merge_request: 7930 -author: diff --git a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml deleted file mode 100644 index 7afc794866b6ab59f9ba849ec54e16493a5c8f42..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Centered Accept Merge Request button within MR widget and added padding for - viewports smaller than 768px -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml deleted file mode 100644 index 52de34478f04675fcc5ee1747da8ba9ee7b792fe..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added top margin to Build status page header for mobile views -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml deleted file mode 100644 index 7f1c417bc777a57e0ed8da1da8b8fdf005632dde..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" - on MR pages' -merge_request: -author: diff --git a/changelogs/unreleased/25264-ref-commit.yml b/changelogs/unreleased/25264-ref-commit.yml deleted file mode 100644 index 13a33da9801c7ad6b6c5f91d16599dbee6667187..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25264-ref-commit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change ref property to commitRef in vue commit component -merge_request: 7901 -author: diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml deleted file mode 100644 index 79cb2c6d8432b004f17400433fd92aeb5587a4e4..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix comments activity tab visibility condition' -merge_request: 7913 -author: Rydkin Maxim diff --git a/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml b/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml new file mode 100644 index 0000000000000000000000000000000000000000..0c9853de3b6ee09ec061e126007bdb91c7f04d77 --- /dev/null +++ b/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml @@ -0,0 +1,4 @@ +--- +title: Added number_with_delimiter to counter on milestone panels +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml deleted file mode 100644 index 567294fe5f7940e3e62d2701a4be561fa8920507..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25294-remove-signed-out-msg.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'fix: removed signed_out notification' -merge_request: 7958 -author: jnoortheen diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml deleted file mode 100644 index 0770f9752a030520e9695f49595bfbfb2b92bf7c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed Housekeeping button on project settings page to default styling -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml b/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml new file mode 100644 index 0000000000000000000000000000000000000000..13d3476fe39af8f4f5e972f2760f4a96ff4efa46 --- /dev/null +++ b/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml @@ -0,0 +1,4 @@ +--- +title: Adds label to Environments "Date Created" +merge_request: 8376 +author: Saad Shahd diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml deleted file mode 100644 index 45a71b55b3b16dea4f86c0c3a92a02d045ef6d2f..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25374-svg-as-prop.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table" -merge_request: 7992 -author: diff --git a/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml b/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml new file mode 100644 index 0000000000000000000000000000000000000000..e60c42cdfba11dfc160c2087934bda6bd8cbbe37 --- /dev/null +++ b/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml @@ -0,0 +1,4 @@ +--- +title: Change status colors of runners to better defaults +merge_request: +author: diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml deleted file mode 100644 index 4c11fe1622eb5ad3d2ce499e3936267b46e2db16..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25482-fix-api-sudo.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Memoize the current_user so that sudo can work properly' -merge_request: 8017 -author: diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml deleted file mode 100644 index d6c92014bea37caf8be3e28f481cb87523b8010b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/25483-broken-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab -merge_request: 8009 -author: diff --git a/changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml b/changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml new file mode 100644 index 0000000000000000000000000000000000000000..18d3ac050ae7e88752450df8c500226de936cbc4 --- /dev/null +++ b/changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml @@ -0,0 +1,4 @@ +--- +title: Resolves overflow in compare branch and tags dropdown +merge_request: 8118 +author: diff --git a/changelogs/unreleased/25678-remove-user-build.yml b/changelogs/unreleased/25678-remove-user-build.yml new file mode 100644 index 0000000000000000000000000000000000000000..873e637d6708cb3d7888ede1e38a13c11fc6c4bc --- /dev/null +++ b/changelogs/unreleased/25678-remove-user-build.yml @@ -0,0 +1,4 @@ +--- +title: remove build_user +merge_request: 8162 +author: Arsenev Vladislav diff --git a/changelogs/unreleased/25701-standardize-text-colors.yml b/changelogs/unreleased/25701-standardize-text-colors.yml new file mode 100644 index 0000000000000000000000000000000000000000..a48ca6c187dec557714348c751b2eb21f393d739 --- /dev/null +++ b/changelogs/unreleased/25701-standardize-text-colors.yml @@ -0,0 +1,4 @@ +--- +title: 25701 standardize text colors +merge_request: +author: diff --git a/changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml b/changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml new file mode 100644 index 0000000000000000000000000000000000000000..850e98518a6e8f25ecb17c80c091cb33a034e748 --- /dev/null +++ b/changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml @@ -0,0 +1,3 @@ +--- +title: Replace wording for slash command confirmation message +merge_request: 8123 diff --git a/changelogs/unreleased/25725-remove-window-object.yml b/changelogs/unreleased/25725-remove-window-object.yml new file mode 100644 index 0000000000000000000000000000000000000000..c64b71ddd33e476c678b655e424d7e510a8617c7 --- /dev/null +++ b/changelogs/unreleased/25725-remove-window-object.yml @@ -0,0 +1,4 @@ +--- +title: Removes unneeded `window` declaration in environments related code +merge_request: 8456 +author: diff --git a/changelogs/unreleased/25776-alerts-should-be-responsive.yml b/changelogs/unreleased/25776-alerts-should-be-responsive.yml new file mode 100644 index 0000000000000000000000000000000000000000..15006523d3eb87c67b568908375f8dfa0480b926 --- /dev/null +++ b/changelogs/unreleased/25776-alerts-should-be-responsive.yml @@ -0,0 +1,4 @@ +--- +title: Changed alerts to be responsive, centered text on smaller viewports +merge_request: 8424 +author: Connor Smallman diff --git a/changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml b/changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml new file mode 100644 index 0000000000000000000000000000000000000000..c82bacd8bcdfcfa64596b237543481977c489d2a --- /dev/null +++ b/changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml @@ -0,0 +1,4 @@ +--- +title: re-enable change username button after failure +merge_request: 8332 +author: diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml new file mode 100644 index 0000000000000000000000000000000000000000..dd0f93e176f84b2942997f437e9aff45fe4b804f --- /dev/null +++ b/changelogs/unreleased/25898-ci-icon-color-mr.yml @@ -0,0 +1,4 @@ +--- +title: Adds CSS class to status icon on MR widget to prevent non-colored icon +merge_request: 8219 +author: diff --git a/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml b/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml new file mode 100644 index 0000000000000000000000000000000000000000..c28cf7a0f8671c9388ecb74afcbcc2d2536c7119 --- /dev/null +++ b/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml @@ -0,0 +1,4 @@ +--- +title: Change earlier to task_status_short to avoid titlebar line wraps +merge_request: +author: diff --git a/changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml b/changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml new file mode 100644 index 0000000000000000000000000000000000000000..b753c8233489377f1bad977c5c342383907c6141 --- /dev/null +++ b/changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml @@ -0,0 +1,4 @@ +--- +title: Use original casing for build action text +merge_request: 8387 +author: diff --git a/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml new file mode 100644 index 0000000000000000000000000000000000000000..206be8fe3cb769e5925e195886c099c2b0debfdb --- /dev/null +++ b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml @@ -0,0 +1,5 @@ +--- +title: Combined the settings options project members and groups into a single one + called members +merge_request: +author: diff --git a/changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml b/changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml new file mode 100644 index 0000000000000000000000000000000000000000..e05e2dd6fede7061c7045b586689342f4d32f1c8 --- /dev/null +++ b/changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml @@ -0,0 +1,4 @@ +--- +title: Move award emoji's out of the discussion tab for merge requests +merge_request: +author: diff --git a/changelogs/unreleased/26014-fix-update-doc.yml b/changelogs/unreleased/26014-fix-update-doc.yml new file mode 100644 index 0000000000000000000000000000000000000000..419c032cb0f966683e4b6f1f9be7c8fec731e187 --- /dev/null +++ b/changelogs/unreleased/26014-fix-update-doc.yml @@ -0,0 +1,4 @@ +--- +title: Re-order update steps in the 8.14 -> 8.15 upgrade guide +merge_request: +author: diff --git a/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml b/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml new file mode 100644 index 0000000000000000000000000000000000000000..85440eb86f9f2dae009c36c101b1acdc11e25f18 --- /dev/null +++ b/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml @@ -0,0 +1,4 @@ +--- +title: Don't instrument 405 Grape calls +merge_request: 8445 +author: diff --git a/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml b/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml new file mode 100644 index 0000000000000000000000000000000000000000..cde0d114d7cdd4117d8ffcae50a5012cb6287c04 --- /dev/null +++ b/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml @@ -0,0 +1,4 @@ +--- +title: Scroll to bottom on build completion if autoscroll was active +merge_request: 8391 +author: diff --git a/changelogs/unreleased/26129-add-link-to-branches-page.yml b/changelogs/unreleased/26129-add-link-to-branches-page.yml new file mode 100644 index 0000000000000000000000000000000000000000..aceb92dbb9c0535a2fbb7d1899e554fb20fa3483 --- /dev/null +++ b/changelogs/unreleased/26129-add-link-to-branches-page.yml @@ -0,0 +1,4 @@ +--- +title: Convert project setting text into protected branch path link +merge_request: 8377 +author: Ken Ding diff --git a/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml b/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml new file mode 100644 index 0000000000000000000000000000000000000000..40183f8d3fa5dc2be2df8df0b0defcda0d3f16c3 --- /dev/null +++ b/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml @@ -0,0 +1,4 @@ +--- +title: Make CTRL+Enter submits a new merge request +merge_request: 8360 +author: Saad Shahd diff --git a/changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml b/changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml new file mode 100644 index 0000000000000000000000000000000000000000..242a77b5d48afc0961ac7244641443a13b03c93b --- /dev/null +++ b/changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml @@ -0,0 +1,4 @@ +--- +title: display merge request discussion tab for empty branches +merge_request: 8347 +author: diff --git a/changelogs/unreleased/26192-fixes-too-short-input.yml b/changelogs/unreleased/26192-fixes-too-short-input.yml new file mode 100644 index 0000000000000000000000000000000000000000..ff707f4694d3d694e04d5075ba3eb4f1eeedd920 --- /dev/null +++ b/changelogs/unreleased/26192-fixes-too-short-input.yml @@ -0,0 +1,4 @@ +--- +title: Fixes too short input for placeholder message in commit listing page +merge_request: 8367 +author: diff --git a/changelogs/unreleased/26207-add-hover-animations.yml b/changelogs/unreleased/26207-add-hover-animations.yml new file mode 100644 index 0000000000000000000000000000000000000000..12a69d04717440b091ed22d6c516ef561fdcb8f2 --- /dev/null +++ b/changelogs/unreleased/26207-add-hover-animations.yml @@ -0,0 +1,4 @@ +--- +title: Add various hover animations throughout the application +merge_request: +author: diff --git a/changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml b/changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml new file mode 100644 index 0000000000000000000000000000000000000000..28981291132a4a6541bec4072530b297c587d4cd --- /dev/null +++ b/changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml @@ -0,0 +1,4 @@ +--- +title: Precompile all JavaScript fixtures +merge_request: 8384 +author: diff --git a/changelogs/unreleased/26238-buttons-not-accessible.yml b/changelogs/unreleased/26238-buttons-not-accessible.yml new file mode 100644 index 0000000000000000000000000000000000000000..34d38d45709b6c94da1f0e46778c73e5e895ad2b --- /dev/null +++ b/changelogs/unreleased/26238-buttons-not-accessible.yml @@ -0,0 +1,4 @@ +--- +title: Fixes buttons not being accessible via the keyboard when creating new group +merge_request: 8469 +author: diff --git a/changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml b/changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml new file mode 100644 index 0000000000000000000000000000000000000000..37bd7e46b4950e589831375b859186494abf2b2b --- /dev/null +++ b/changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml @@ -0,0 +1,4 @@ +--- +title: Fix Commits API to accept a Project path upon POST +merge_request: +author: diff --git a/changelogs/unreleased/26352-user-dropdown-settings.yml b/changelogs/unreleased/26352-user-dropdown-settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..19bd47b8673d332dab22d480e3bc7e4a00f069ec --- /dev/null +++ b/changelogs/unreleased/26352-user-dropdown-settings.yml @@ -0,0 +1,4 @@ +--- +title: 26352 Change Profile settings to User / Settings +merge_request: +author: diff --git a/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml b/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml new file mode 100644 index 0000000000000000000000000000000000000000..43afdf450131ae7f07cbde39d161f097be3a0257 --- /dev/null +++ b/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml @@ -0,0 +1,4 @@ +--- +title: Display project avatars on Admin Area and Projects pages for mobile views +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml b/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml new file mode 100644 index 0000000000000000000000000000000000000000..b4aef8fe3da28e50671f1c43e07ed91deb6ab54d --- /dev/null +++ b/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml @@ -0,0 +1,4 @@ +--- +title: Make play button on Pipelines page accessible via keyboard +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml b/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml new file mode 100644 index 0000000000000000000000000000000000000000..83f6233dd887a1166abaccc236137602a790a247 --- /dev/null +++ b/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml @@ -0,0 +1,5 @@ +--- +title: Made download artifacts button accessible via keyboard by changing it from + an anchor tag to an actual button +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/26504-mr-discussion-btn.yml b/changelogs/unreleased/26504-mr-discussion-btn.yml new file mode 100644 index 0000000000000000000000000000000000000000..dec74ec61b15948e9b549f78704e51bbbe44411c --- /dev/null +++ b/changelogs/unreleased/26504-mr-discussion-btn.yml @@ -0,0 +1,4 @@ +--- +title: 26504 Fix styling of MR jump to discussion button +merge_request: +author: diff --git a/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml b/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml new file mode 100644 index 0000000000000000000000000000000000000000..5891a5ef6e889c814cd7f952cff30dfe91f6549a --- /dev/null +++ b/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml @@ -0,0 +1,4 @@ +--- +title: Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route +merge_request: 8544 +author: diff --git a/changelogs/unreleased/26615-pipeline-status-cell.yml b/changelogs/unreleased/26615-pipeline-status-cell.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a19b041e6383c5fc07da31d2b82a1c9e181b524 --- /dev/null +++ b/changelogs/unreleased/26615-pipeline-status-cell.yml @@ -0,0 +1,4 @@ +--- +title: Fixes pipeline status cell is too wide by adding missing classes in table head cells +merge_request: 8549 +author: diff --git a/changelogs/unreleased/26616-fix-search-group-project-filters.yml b/changelogs/unreleased/26616-fix-search-group-project-filters.yml new file mode 100644 index 0000000000000000000000000000000000000000..0fd0dbbfc242f3f881d9d7334e6a3a482abf2b06 --- /dev/null +++ b/changelogs/unreleased/26616-fix-search-group-project-filters.yml @@ -0,0 +1,4 @@ +--- +title: Fix search group/project filtering to show results +merge_request: +author: diff --git a/changelogs/unreleased/26773-fix-project-statistics-repository-size.yml b/changelogs/unreleased/26773-fix-project-statistics-repository-size.yml new file mode 100644 index 0000000000000000000000000000000000000000..8ce9bbcb3a995912ce4ac51043c6a157ca26d687 --- /dev/null +++ b/changelogs/unreleased/26773-fix-project-statistics-repository-size.yml @@ -0,0 +1,4 @@ +--- +title: Adjust ProjectStatistic#repository_size with values saved as MB +merge_request: 8616 +author: diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml deleted file mode 100644 index 560bc6a4f1343b7130547dce4c486766f5b28ae6..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/4269-public-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow public access to some Project API endpoints -merge_request: 7843 -author: diff --git a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml deleted file mode 100644 index 9dd04d3f089a98a41f05108455ea94885542fede..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Add setting to enable/disable HTML emails -merge_request: 7749 -author: diff --git a/changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml b/changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml new file mode 100644 index 0000000000000000000000000000000000000000..74412c323756283d513645083fd300ff9d07f836 --- /dev/null +++ b/changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml @@ -0,0 +1,4 @@ +--- +title: Fixes issue boards list colored top border visual glitch +merge_request: 7898 +author: Pier Paolo Ramon diff --git a/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml b/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml new file mode 100644 index 0000000000000000000000000000000000000000..4d83d744be7ba9d86c8ee84c1c2381f666d74b81 --- /dev/null +++ b/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml @@ -0,0 +1,4 @@ +--- +title: Search bar redesign first iteration +merge_request: 7345 +author: diff --git a/changelogs/unreleased/add_email_password_confirmation.yml b/changelogs/unreleased/add_email_password_confirmation.yml new file mode 100644 index 0000000000000000000000000000000000000000..92f9b9b7a6d15e683cd418e71a988d85fecd5d88 --- /dev/null +++ b/changelogs/unreleased/add_email_password_confirmation.yml @@ -0,0 +1,4 @@ +--- +title: Add email confirmation field to registration form +merge_request: 7432 +author: diff --git a/changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml b/changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml new file mode 100644 index 0000000000000000000000000000000000000000..8d5a94c3aa8b97e948cfba557e0c084a95758ea1 --- /dev/null +++ b/changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml @@ -0,0 +1,4 @@ +--- +title: Removed bottom padding from merge manually from CLI because of repositioning award emoji's +merge_request: +author: diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml deleted file mode 100644 index 7989f94e528dda8b1895d02870d6185f8becf71c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/allow-more-filenames.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow all alphanumeric characters in file names -merge_request: 8002 -author: winniehell diff --git a/changelogs/unreleased/allow_plus_sign_for_snippets.yml b/changelogs/unreleased/allow_plus_sign_for_snippets.yml new file mode 100644 index 0000000000000000000000000000000000000000..62d9dd74d07ee81bf6ae419b880418a829f8874a --- /dev/null +++ b/changelogs/unreleased/allow_plus_sign_for_snippets.yml @@ -0,0 +1,4 @@ +--- +title: Allow to use + symbol in filenames +merge_request: 6644 +author: blackst0ne diff --git a/changelogs/unreleased/api-branch-status.yml b/changelogs/unreleased/api-branch-status.yml deleted file mode 100644 index c5763345a2201b8d4692da34119bfde4d3fa0f89..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-branch-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Expose merge status for branch API' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml deleted file mode 100644 index 5f4cee450b9adf081d26c1ffcc572dbc82fbcbe6..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-cherry-pick.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Ability to cherry pick a commit' -merge_request: 8047 -author: Robert Schilling diff --git a/changelogs/unreleased/api-delete-group-share.yml b/changelogs/unreleased/api-delete-group-share.yml deleted file mode 100644 index 26cfb35bba324c9cb9a276516800ea1af7de14f7..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-delete-group-share.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Add ability to unshare a project from a group' -merge_request: 7662 -author: Robert Schilling diff --git a/changelogs/unreleased/api-expose-commiter-details.yml b/changelogs/unreleased/api-expose-commiter-details.yml deleted file mode 100644 index 5ee34adc5c94a372d3add28a76c6e42ad0fd6e4e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-expose-commiter-details.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Expose committer details for commits' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml deleted file mode 100644 index d1b6507aedb8c37a890c404f698dd8a5b79da851..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-remove-source-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Ability to set ''should_remove_source_branch'' on merge requests' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml deleted file mode 100644 index 54c8de610a6c09c045b0389d1ed49616206150fa..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/api-simple-group-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Simple representation of group''s projects' -merge_request: 8060 -author: Robert Schilling diff --git a/changelogs/unreleased/asciidoctor-plantuml.yml b/changelogs/unreleased/asciidoctor-plantuml.yml new file mode 100644 index 0000000000000000000000000000000000000000..ba6ef7c0800268e9ce721667b20c6f1b344d532d --- /dev/null +++ b/changelogs/unreleased/asciidoctor-plantuml.yml @@ -0,0 +1,4 @@ +--- +title: Add support for PlantUML diagrams in AsciiDoc documents. +merge_request: 7810 +author: Horacio Sanson diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml deleted file mode 100644 index 1f9904c069172cc816582e4058d1efe405e7f6be..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/awards_handler.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for awards_handler_spec -merge_request: 7661 -author: winniehell diff --git a/changelogs/unreleased/badge-color-on-white-bg.yml b/changelogs/unreleased/badge-color-on-white-bg.yml new file mode 100644 index 0000000000000000000000000000000000000000..680d7ff11f0c0f7fca6fcf977e94f762e02a5d36 --- /dev/null +++ b/changelogs/unreleased/badge-color-on-white-bg.yml @@ -0,0 +1,4 @@ +--- +title: Added lighter count badge background-color for on white backgrounds +merge_request: 7873 +author: diff --git a/changelogs/unreleased/bug-project-feature-compatibility.yml b/changelogs/unreleased/bug-project-feature-compatibility.yml new file mode 100644 index 0000000000000000000000000000000000000000..2124ee085e0de8c9d1d0bbda383e61666807e8dc --- /dev/null +++ b/changelogs/unreleased/bug-project-feature-compatibility.yml @@ -0,0 +1,5 @@ +--- +title: Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility` + concern. +merge_request: 8552 +author: diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml deleted file mode 100644 index f70607df7a18b4cbc85ab6fb00cdffe7184d86c0..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/chomp-git-status-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: For single line git commit messages, the close quote should be on the same - line as the open quote -merge_request: -author: diff --git a/changelogs/unreleased/cleanup-common_utils-js.yml b/changelogs/unreleased/cleanup-common_utils-js.yml deleted file mode 100644 index 54d81b76c28c146994fcc5a39020016b7bef1c9e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/cleanup-common_utils-js.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Clean up common_utils.js -merge_request: 7318 -author: winniehell diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml new file mode 100644 index 0000000000000000000000000000000000000000..6aa4a5664e7e592b0fba3627f0ddd2c5b8269526 --- /dev/null +++ b/changelogs/unreleased/clipboard-button-commit-sha.yml @@ -0,0 +1,3 @@ +--- +title: 'Copy commit SHA to clipboard' +merge_request: 8547 diff --git a/changelogs/unreleased/clipboard-button-text.yml b/changelogs/unreleased/clipboard-button-text.yml new file mode 100644 index 0000000000000000000000000000000000000000..dc93da6042679ba46ef0c5d27bdc697c8ef98a5d --- /dev/null +++ b/changelogs/unreleased/clipboard-button-text.yml @@ -0,0 +1,3 @@ +--- +title: 'Copy <some text> to clipboard' +merge_request: 8535 diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml deleted file mode 100644 index 824c1c88a60df4192ff9f18e4dacad1d068b760f..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/comments-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for notes_spec -merge_request: 7683 -author: winniehell diff --git a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml deleted file mode 100644 index f0d9ff0c34ffad5a38fe97e7da20ccd645d3fc8a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create dynamic fixture for build_spec -merge_request: 7589 -author: winniehell diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml deleted file mode 100644 index e713e2dc4243df604e8a964cd3d1b170da9c7034..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/destroy-session.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Destroy a user's session when they delete their own account -merge_request: -author: diff --git a/changelogs/unreleased/dev-issue-24554.yml b/changelogs/unreleased/dev-issue-24554.yml deleted file mode 100644 index 0bb362b93255127ff3edd3adde1c9915a7842f09..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/dev-issue-24554.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Edit help text to clarify annotated tag creation. -merge_request: -author: Liz Lam diff --git a/changelogs/unreleased/didemacet-ci-lint-page.yml b/changelogs/unreleased/didemacet-ci-lint-page.yml new file mode 100644 index 0000000000000000000000000000000000000000..07386321c9dc875e4a8dfe5fa0371b9b389b66c6 --- /dev/null +++ b/changelogs/unreleased/didemacet-ci-lint-page.yml @@ -0,0 +1,4 @@ +--- +title: Change CI template linter textarea with Ace Editor +merge_request: 8452 +author: Didem Acet diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml deleted file mode 100644 index 12b1460f388c8c97e1a12355a59e3e76afbb9527..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not reload diff for merge request made from fork when target branch in fork is updated -merge_request: 7973 -author: diff --git a/changelogs/unreleased/dot-in-project-queries.yml b/changelogs/unreleased/dot-in-project-queries.yml new file mode 100644 index 0000000000000000000000000000000000000000..fc48dc7b74d808b54cc55c6bc478f5cfbbd6f12d --- /dev/null +++ b/changelogs/unreleased/dot-in-project-queries.yml @@ -0,0 +1,4 @@ +--- +title: Allow API query to find projects with dots in their name +merge_request: +author: Bruno Melli diff --git a/changelogs/unreleased/dz-allow-nested-group-routing.yml b/changelogs/unreleased/dz-allow-nested-group-routing.yml deleted file mode 100644 index 9d8e6e1791493b1772e2a8feca9ebf5dd36968d1..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/dz-allow-nested-group-routing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add nested groups support to the routing -merge_request: 7459 -author: diff --git a/changelogs/unreleased/dz-nested-group-misc.yml b/changelogs/unreleased/dz-nested-group-misc.yml new file mode 100644 index 0000000000000000000000000000000000000000..9c9d0b1c644afd52fe81fcb7fd79f3e83cd4b275 --- /dev/null +++ b/changelogs/unreleased/dz-nested-group-misc.yml @@ -0,0 +1,4 @@ +--- +title: Show nested groups tab on group page +merge_request: 8308 +author: diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml deleted file mode 100644 index c227c5a8ea5d7f692028321c05199e889f65bc9a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/dz-nested-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add nested groups support on data level -merge_request: -author: diff --git a/changelogs/unreleased/dz-rename-invalid-users.yml b/changelogs/unreleased/dz-rename-invalid-users.yml new file mode 100644 index 0000000000000000000000000000000000000000..f420b069531231d4f9d98b5a18f0d3635e2aac78 --- /dev/null +++ b/changelogs/unreleased/dz-rename-invalid-users.yml @@ -0,0 +1,4 @@ +--- +title: Rename users with namespace ending with .git +merge_request: 8309 +author: diff --git a/changelogs/unreleased/emoji-btn-disabled.yml b/changelogs/unreleased/emoji-btn-disabled.yml deleted file mode 100644 index a18b553d5137d0a21987f506649ca55badc5e847..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/emoji-btn-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disabled emoji buttons when user is not logged in -merge_request: -author: diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml deleted file mode 100644 index 9c52e53c3b4762c9964415b08bc715bb4ced49e0..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable AsciiDoctor admonition icons -merge_request: 7812 -author: Horacio Sanson diff --git a/changelogs/unreleased/env-var-in-redis-config.yml b/changelogs/unreleased/env-var-in-redis-config.yml new file mode 100644 index 0000000000000000000000000000000000000000..561ea7f514ef46482c79e9433dbf9ffd2edf8400 --- /dev/null +++ b/changelogs/unreleased/env-var-in-redis-config.yml @@ -0,0 +1,4 @@ +--- +title: Allow to use ENV variables in redis config +merge_request: 8073 +author: Semyon Pupkov diff --git a/changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml b/changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml new file mode 100644 index 0000000000000000000000000000000000000000..0fd590a877b2f1f9d0daf8709d84d25fac006a40 --- /dev/null +++ b/changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml @@ -0,0 +1,4 @@ +--- +title: Allow to add deploy keys with write-access +merge_request: 5807 +author: Ali Ibrahim diff --git a/changelogs/unreleased/feature-admin-merge-groups-and-projects.yml b/changelogs/unreleased/feature-admin-merge-groups-and-projects.yml new file mode 100644 index 0000000000000000000000000000000000000000..0c0b74b686a78cd2077a9f15f2178dc86263accd --- /dev/null +++ b/changelogs/unreleased/feature-admin-merge-groups-and-projects.yml @@ -0,0 +1,4 @@ +--- +title: Merged the 'Groups' and 'Projects' tabs when viewing user profiles +merge_request: 8323 +author: James Gregory diff --git a/changelogs/unreleased/feature-admin-user-groups-link.yml b/changelogs/unreleased/feature-admin-user-groups-link.yml deleted file mode 100644 index b89c08f82d7232a7b294cf1c88c6b53093889808..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/feature-admin-user-groups-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: The admin user projects view now has a clickable group link -merge_request: 7620 -author: James Gregory diff --git a/changelogs/unreleased/feature-log-ldap-to-application-log.yml b/changelogs/unreleased/feature-log-ldap-to-application-log.yml new file mode 100644 index 0000000000000000000000000000000000000000..4cfbc23edb7f81f1a7a7f2d33488d4c0ee4b12a3 --- /dev/null +++ b/changelogs/unreleased/feature-log-ldap-to-application-log.yml @@ -0,0 +1,4 @@ +--- +title: Log LDAP blocking/unblocking events to application log +merge_request: 8042 +author: Markus Koller diff --git a/changelogs/unreleased/feature-more-storage-statistics.yml b/changelogs/unreleased/feature-more-storage-statistics.yml new file mode 100644 index 0000000000000000000000000000000000000000..824fd36dc34dbb4ff0be0aa9a773728c6ca17105 --- /dev/null +++ b/changelogs/unreleased/feature-more-storage-statistics.yml @@ -0,0 +1,4 @@ +--- +title: Add more storage statistics +merge_request: 7754 +author: Markus Koller diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml deleted file mode 100644 index 80c7bb753594eeb5bbb62d1ec690bfd6d0eafb5d..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/features-api-snippets.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Endpoint to expose personal snippets as /snippets' -merge_request: 6373 -author: Bernard Guyzmo Pratz diff --git a/changelogs/unreleased/filename-to-file-path.yml b/changelogs/unreleased/filename-to-file-path.yml new file mode 100644 index 0000000000000000000000000000000000000000..3c6c838595a5196af051051655300b737cff0730 --- /dev/null +++ b/changelogs/unreleased/filename-to-file-path.yml @@ -0,0 +1,3 @@ +--- +title: Rename filename to file path in tooltip of file header in merge request diff +merge_request: 8314 \ No newline at end of file diff --git a/changelogs/unreleased/fill-authorized-projects.yml b/changelogs/unreleased/fill-authorized-projects.yml new file mode 100644 index 0000000000000000000000000000000000000000..e8e33011a15ee27a9df03c65d500cc551a5750ff --- /dev/null +++ b/changelogs/unreleased/fill-authorized-projects.yml @@ -0,0 +1,4 @@ +--- +title: Fill missing authorized projects rows +merge_request: +author: diff --git a/changelogs/unreleased/fix-api-deprecation.yml b/changelogs/unreleased/fix-api-deprecation.yml new file mode 100644 index 0000000000000000000000000000000000000000..90285ddf058935e31147f01b10b1b12c347e1508 --- /dev/null +++ b/changelogs/unreleased/fix-api-deprecation.yml @@ -0,0 +1,4 @@ +--- +title: Fix a Grape deprecation, use `#request_method` instead of `#route_method` +merge_request: +author: diff --git a/changelogs/unreleased/fix-blame-500.yml b/changelogs/unreleased/fix-blame-500.yml new file mode 100644 index 0000000000000000000000000000000000000000..379d81aaa444fec2705db4db0e352d863337ff79 --- /dev/null +++ b/changelogs/unreleased/fix-blame-500.yml @@ -0,0 +1,4 @@ +--- +title: Fix blame 500 error on invalid path. +merge_request: 25761 +author: Jeff Stubler diff --git a/changelogs/unreleased/fix-boards-search-typo.yml b/changelogs/unreleased/fix-boards-search-typo.yml new file mode 100644 index 0000000000000000000000000000000000000000..0c083fc0d10746d22be5b595d812a743eefef991 --- /dev/null +++ b/changelogs/unreleased/fix-boards-search-typo.yml @@ -0,0 +1,4 @@ +--- +title: 'Fix typo: seach to search' +merge_request: 8370 +author: diff --git a/changelogs/unreleased/fix-broken-url-on-group-avatar.yml b/changelogs/unreleased/fix-broken-url-on-group-avatar.yml new file mode 100644 index 0000000000000000000000000000000000000000..7ce22b4826e740fcfe2d812f1d440d76b4b71466 --- /dev/null +++ b/changelogs/unreleased/fix-broken-url-on-group-avatar.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken url on group avatar +merge_request: 8464 +author: hogewest diff --git a/changelogs/unreleased/fix-build-sort-order.yml b/changelogs/unreleased/fix-build-sort-order.yml new file mode 100644 index 0000000000000000000000000000000000000000..a6d6371f69acb972417ee491f71f5e54e229c5d5 --- /dev/null +++ b/changelogs/unreleased/fix-build-sort-order.yml @@ -0,0 +1,4 @@ +--- +title: Sort numbers in build names more intelligently +merge_request: 8277 +author: diff --git a/changelogs/unreleased/fix-cancelling-pipelines.yml b/changelogs/unreleased/fix-cancelling-pipelines.yml deleted file mode 100644 index c21e663093aef6c42ee17c25ef51f4a2415e05af..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/fix-cancelling-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix cancelling created or external pipelines -merge_request: 7508 -author: diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml new file mode 100644 index 0000000000000000000000000000000000000000..a87b7612217f8fced0a056cfd5f783257a189594 --- /dev/null +++ b/changelogs/unreleased/fix-copy-issues-empty-state.yml @@ -0,0 +1,4 @@ +--- +title: Improve copy in Issue Tracker empty state +merge_request: 8202 +author: diff --git a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml deleted file mode 100644 index e37841e80c328ae76fcf633fbf53dc66f3a2ab29..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create builds in transaction to avoid empty pipelines -merge_request: 7742 -author: diff --git a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml deleted file mode 100644 index 0d11969575aa741e1973d524e08e72ab7b070cfb..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use authorized projects in ProjectTeam -merge_request: -author: diff --git a/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml new file mode 100644 index 0000000000000000000000000000000000000000..3d8cf1c74a2044854359690fa60c228437079f6a --- /dev/null +++ b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml @@ -0,0 +1,4 @@ +--- +title: Hide build artifacts keep button if operation is not allowed +merge_request: 8501 +author: diff --git a/changelogs/unreleased/fix-light-hr-in-descriptions.yml b/changelogs/unreleased/fix-light-hr-in-descriptions.yml new file mode 100644 index 0000000000000000000000000000000000000000..8efd471e416287ddaacdbefa9262ce19327c562e --- /dev/null +++ b/changelogs/unreleased/fix-light-hr-in-descriptions.yml @@ -0,0 +1,4 @@ +--- +title: Darkened hr border color in descriptions because of update of bootstrap +merge_request: 8333 +author: diff --git a/changelogs/unreleased/fix-milestone-summary.yml b/changelogs/unreleased/fix-milestone-summary.yml deleted file mode 100644 index 3045a15054ce9ec3e7fb36bfcae7a084176c0b11..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/fix-milestone-summary.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Displays milestone remaining days only when it's present -merge_request: -author: diff --git a/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml b/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml new file mode 100644 index 0000000000000000000000000000000000000000..bc2068b8177dda8f9c456ca14d88fae529e3c20c --- /dev/null +++ b/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml @@ -0,0 +1,4 @@ +--- +title: Remove extra orphaned rows when removing stray namespaces +merge_request: 7841 +author: diff --git a/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml b/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml new file mode 100644 index 0000000000000000000000000000000000000000..980665377234f6321493fd870867a3d04cc9503a --- /dev/null +++ b/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml @@ -0,0 +1,4 @@ +--- +title: 'API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`' +merge_request: 8457 +author: Panagiotis Atmatzidis, David Eisner diff --git a/changelogs/unreleased/fix-project-delete-tooltip.yml b/changelogs/unreleased/fix-project-delete-tooltip.yml new file mode 100644 index 0000000000000000000000000000000000000000..42fd9c32519e080d244ddc38c913f392afa2f3e0 --- /dev/null +++ b/changelogs/unreleased/fix-project-delete-tooltip.yml @@ -0,0 +1,4 @@ +--- +title: Fix project queued for deletion re-creation tooltip +merge_request: +author: diff --git a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml deleted file mode 100644 index f8acc6ef8ad04f8cd05f62fefc5a522e65f5c29c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rename Merge When Build Succeeds to Merge When Pipeline Succeeds -merge_request: 7135 -author: diff --git a/changelogs/unreleased/fix-serialized-commit-path.yml b/changelogs/unreleased/fix-serialized-commit-path.yml new file mode 100644 index 0000000000000000000000000000000000000000..4e4df5038741ccccaac41d94304f803534bf1b71 --- /dev/null +++ b/changelogs/unreleased/fix-serialized-commit-path.yml @@ -0,0 +1,4 @@ +--- +title: Fix links to commits pages on pipelines list page +merge_request: 8558 +author: diff --git a/changelogs/unreleased/fix-timezone-due-date-picker.yml b/changelogs/unreleased/fix-timezone-due-date-picker.yml new file mode 100644 index 0000000000000000000000000000000000000000..2e6b71c70ca68b2a13f4a77351f1d2f2df79b038 --- /dev/null +++ b/changelogs/unreleased/fix-timezone-due-date-picker.yml @@ -0,0 +1,4 @@ +--- +title: Fix date inconsistency on due date picker +merge_request: 7422 +author: Giuliano Varriale diff --git a/changelogs/unreleased/fix-user-api-confirm-param.yml b/changelogs/unreleased/fix-user-api-confirm-param.yml new file mode 100644 index 0000000000000000000000000000000000000000..4264257663480b762e0698a1fd5fabc712e47ed1 --- /dev/null +++ b/changelogs/unreleased/fix-user-api-confirm-param.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 error when POSTing to Users API with optional confirm param +merge_request: +author: diff --git a/changelogs/unreleased/get_last_used_date_of_ssh_key.yml b/changelogs/unreleased/get_last_used_date_of_ssh_key.yml new file mode 100644 index 0000000000000000000000000000000000000000..b753949922c7ffaa7dbb125e71a722e3fd1637f3 --- /dev/null +++ b/changelogs/unreleased/get_last_used_date_of_ssh_key.yml @@ -0,0 +1,4 @@ +--- +title: Record and show last used date of SSH Keys +merge_request: 8113 +author: Vincent Wong diff --git a/changelogs/unreleased/glm-shorthand-reference.yml b/changelogs/unreleased/glm-shorthand-reference.yml deleted file mode 100644 index 6d60f23c798fbd409a6fdd5eeceabe0dc5390a6a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/glm-shorthand-reference.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add shorthand support to gitlab markdown references -merge_request: 7255 -author: Oswaldo Ferreira diff --git a/changelogs/unreleased/group-members-in-project-members-view.yml b/changelogs/unreleased/group-members-in-project-members-view.yml deleted file mode 100644 index 415e2b6b1e20563a20ac67e4b84284649d35e0f9..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/group-members-in-project-members-view.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Shows group members in project members list -merge_request: -author: diff --git a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml deleted file mode 100644 index 73d8a52e001333c216991f05aebeea3378de4107..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add git diff context to notifications of new notes on merge requests -merge_request: -author: Heidi Hoopes diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml deleted file mode 100644 index 8f8bbc519630b3ba636923fea2a9c959ed6933d4..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/html-safe-diff-line-content.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't accidentally mark unsafe diff lines as HTML safe -merge_request: -author: diff --git a/changelogs/unreleased/i--25814-500-error.yml b/changelogs/unreleased/i--25814-500-error.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd55ede84c8b29a02fb9512dada2647227d242aa --- /dev/null +++ b/changelogs/unreleased/i--25814-500-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix Compare page throws 500 error when any branch/reference is not selected +merge_request: 8492 +author: Martin Cabrera diff --git a/changelogs/unreleased/improve-invite-accept-page.yml b/changelogs/unreleased/improve-invite-accept-page.yml deleted file mode 100644 index 8a09a5ae42f0f385a72edca877d66c568d966294..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/improve-invite-accept-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add note to the invite page when the logged in user email is not the same as the invitation -merge_request: -author: diff --git a/changelogs/unreleased/issuable_filters_present-refactor.yml b/changelogs/unreleased/issuable_filters_present-refactor.yml deleted file mode 100644 index c131f9cb68e22768193bcfadaefb73b6da7e1739..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issuable_filters_present-refactor.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor issuable_filters_present to reduce duplications -merge_request: 7776 -author: Semyon Pupkov diff --git a/changelogs/unreleased/issue-24534.yml b/changelogs/unreleased/issue-24534.yml deleted file mode 100644 index 14d6730d3f6488455a526e0d4f12e473379c8615..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue-24534.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary sentences for status codes in the API documentation -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue-boards-animate.yml b/changelogs/unreleased/issue-boards-animate.yml new file mode 100644 index 0000000000000000000000000000000000000000..28394aec3d590f4516a34a65d46b55c17baea7e2 --- /dev/null +++ b/changelogs/unreleased/issue-boards-animate.yml @@ -0,0 +1,4 @@ +--- +title: Added animations to issue boards interactions +merge_request: +author: diff --git a/changelogs/unreleased/issue-boards-scrollable-element.yml b/changelogs/unreleased/issue-boards-scrollable-element.yml deleted file mode 100644 index 90edc30e7917213aa937b836d5743f425cd7e666..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue-boards-scrollable-element.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards scrolling with a lot of lists & issues -merge_request: -author: diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml deleted file mode 100644 index a3b08bde6e7a395ea32664315bfebcd775f41cf3..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue-events-filter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add issue events filter and make all really show all events -merge_request: 7673 -author: Oxan van Leeuwen diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml deleted file mode 100644 index 9c15c4368769af51b184758f846e5d0580921ffc..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue_13270.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to delete tag release note -merge_request: -author: diff --git a/changelogs/unreleased/issue_22664.yml b/changelogs/unreleased/issue_22664.yml new file mode 100644 index 0000000000000000000000000000000000000000..18a8d9ec6be4ffca9b2e19e4aff8af8a4d93414b --- /dev/null +++ b/changelogs/unreleased/issue_22664.yml @@ -0,0 +1,4 @@ +--- +title: Check if user can read project before being assigned to issue +merge_request: +author: diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml deleted file mode 100644 index 87310b7529609357e3925404905c057e86443b75..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue_24020.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "fix display hook error message" -merge_request: 7775 -author: basyura diff --git a/changelogs/unreleased/issue_24363.yml b/changelogs/unreleased/issue_24363.yml deleted file mode 100644 index 0298890b4772f0adb16c1b260181c3d75fa37b5c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue_24363.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix appearance in error pages -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue_24748.yml b/changelogs/unreleased/issue_24748.yml deleted file mode 100644 index 4c1df542c539a67869d2921e42811457181471c2..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue_24748.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix title case to sentence case -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue_24958.yml b/changelogs/unreleased/issue_24958.yml deleted file mode 100644 index dbbbbf9d28d89bf6af3c6cecce12c06c88178e9d..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue_24958.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix bad selection on dropdown menu for tags filter -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue_25017.yml b/changelogs/unreleased/issue_25017.yml new file mode 100644 index 0000000000000000000000000000000000000000..09126ae81bcf911b9be2634760469f180a554817 --- /dev/null +++ b/changelogs/unreleased/issue_25017.yml @@ -0,0 +1,4 @@ +--- +title: Show 'too many changes' message for created merge requests when they are too large +merge_request: +author: diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml deleted file mode 100644 index e18b8d6a79b9f8146622c043ec28d2494fc55d48..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issue_25030.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow branch names with dots on API endpoint -merge_request: -author: diff --git a/changelogs/unreleased/issue_25578.yml b/changelogs/unreleased/issue_25578.yml new file mode 100644 index 0000000000000000000000000000000000000000..e10f1d232afb2da9708ff702cc5b5d68e3bfe94b --- /dev/null +++ b/changelogs/unreleased/issue_25578.yml @@ -0,0 +1,4 @@ +--- +title: Fix redirect after update file when user has forked project +merge_request: +author: diff --git a/changelogs/unreleased/issue_25682.yml b/changelogs/unreleased/issue_25682.yml new file mode 100644 index 0000000000000000000000000000000000000000..a50138756ba0464f1894947cde36e7ab2c46b988 --- /dev/null +++ b/changelogs/unreleased/issue_25682.yml @@ -0,0 +1,4 @@ +--- +title: Parse JIRA issue references even if Issue Tracker is disabled +merge_request: +author: diff --git a/changelogs/unreleased/issues-1608-text.yml b/changelogs/unreleased/issues-1608-text.yml deleted file mode 100644 index bef427a1e1e605e0a266fe6e13aa7de3ae723c61..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/issues-1608-text.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: change text around timestamps to make it clear which timestamp is displayed -merge_request: 7860 -author: BM5k diff --git a/changelogs/unreleased/issues-8081.yml b/changelogs/unreleased/issues-8081.yml new file mode 100644 index 0000000000000000000000000000000000000000..82f746937bc592c37f1ef67d9d512fa1a2cca31f --- /dev/null +++ b/changelogs/unreleased/issues-8081.yml @@ -0,0 +1,4 @@ +--- +title: change 'gray' color theme name to 'black' to match the actual color +merge_request: 7908 +author: BM5k diff --git a/changelogs/unreleased/jej-22869.yml b/changelogs/unreleased/jej-22869.yml deleted file mode 100644 index 9d2edcfee42c9626ec30717ba77f96daad64004e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/jej-22869.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix information disclosure in `Projects::BlobController#update` -merge_request: -author: diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml deleted file mode 100644 index 5a4a44b9562f0c794851befc59a14700329123cd..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace MR access checks with use of MergeRequestsFinder -merge_request: -author: diff --git a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml deleted file mode 100644 index 844fba9a1075aacdc0f3a8059377f8e24b7bdfd7..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix missing access checks on issue lookup using IssuableFinder -merge_request: -author: diff --git a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml deleted file mode 100644 index c0b6f50052c0316d4aee75376106a8f9d3472b0e..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace issue access checks with use of IssuableFinder -merge_request: -author: diff --git a/changelogs/unreleased/ldap_maint_task.yml b/changelogs/unreleased/ldap_maint_task.yml new file mode 100644 index 0000000000000000000000000000000000000000..8acffba0ce52c978102ba5e8e0dea2d1b6ed5b82 --- /dev/null +++ b/changelogs/unreleased/ldap_maint_task.yml @@ -0,0 +1,4 @@ +--- +title: Add LDAP Rake task to rename a provider +merge_request: 2181 +author: diff --git a/changelogs/unreleased/login-page-font-size.yml b/changelogs/unreleased/login-page-font-size.yml new file mode 100644 index 0000000000000000000000000000000000000000..e7775006673e62bca5316dfb288534611c88136b --- /dev/null +++ b/changelogs/unreleased/login-page-font-size.yml @@ -0,0 +1,4 @@ +--- +title: Decreases font-size on login page +merge_request: +author: diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml deleted file mode 100644 index b15403d6d623636c9c7974b298066954561ada2a..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/members-dropdowns.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Updated members dropdowns -merge_request: -author: diff --git a/changelogs/unreleased/milestone_start_date.yml b/changelogs/unreleased/milestone_start_date.yml deleted file mode 100644 index 39ac1344329cbb5e676ff285caec66b922f7b351..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/milestone_start_date.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add a starting date to milestones -merge_request: -author: diff --git a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml deleted file mode 100644 index 9de7477c200a0029fddad11f49591e76781660e8..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move abuse report spinach test to rspec -merge_request: 7659 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml deleted file mode 100644 index fb70fa2955a2c6d302ec2744d119c1679d33dce4..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin abuse report spinach test to rspec -merge_request: 7691 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml deleted file mode 100644 index 7dfd741985aa989da27cc324f8c1c6299e12895c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin hooks spinach to rspec -merge_request: 7942 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml deleted file mode 100644 index 696aa8510a0ea6b946f833eba023f80bd4125680..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin logs spinach test to rspec -merge_request: 7945 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml deleted file mode 100644 index a7ec2c2055431ff57c37c41e278c61f0e4f3aefc..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin spam spinach test to Rspec -merge_request: 7708 -author: Semyon Pupkov diff --git a/changelogs/unreleased/mr-origin-7855.yml b/changelogs/unreleased/mr-origin-7855.yml deleted file mode 100644 index 0fdc6153d55b8cc921473bc5f67851dc8f35c327..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/mr-origin-7855.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Provides a sensible default message when adding a README to a project -merge_request: 7903 -author: diff --git a/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml new file mode 100644 index 0000000000000000000000000000000000000000..b8c7b78cf0d630324eedbc7775acb785b3c275a1 --- /dev/null +++ b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml @@ -0,0 +1,4 @@ +--- +title: Fixed merge request tabs dont move when opening collapsed sidebar +merge_request: +author: diff --git a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml new file mode 100644 index 0000000000000000000000000000000000000000..fd17303110733b323c42eb136742a328cb4ea1b0 --- /dev/null +++ b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml @@ -0,0 +1,4 @@ +--- +title: Remove trailing whitespace when generating changelog entry +merge_request: 7948 +author: diff --git a/changelogs/unreleased/pc-add-gitaly-to-architecture.yml b/changelogs/unreleased/pc-add-gitaly-to-architecture.yml new file mode 100644 index 0000000000000000000000000000000000000000..7c18da698df2dd5837903479db22646c805da1e7 --- /dev/null +++ b/changelogs/unreleased/pc-add-gitaly-to-architecture.yml @@ -0,0 +1,4 @@ +--- +title: Add Gitaly to the architecture documentation +merge_request: 8264 +author: Pablo Carranza <pablo@gitlab.com> diff --git a/changelogs/unreleased/pipelines-graph-html-css.yml b/changelogs/unreleased/pipelines-graph-html-css.yml new file mode 100644 index 0000000000000000000000000000000000000000..ff0c3122fdb479f1806d529db15895fb8896c270 --- /dev/null +++ b/changelogs/unreleased/pipelines-graph-html-css.yml @@ -0,0 +1,4 @@ +--- +title: Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown +merge_request: 8443 +author: diff --git a/changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml b/changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml new file mode 100644 index 0000000000000000000000000000000000000000..23230128dc93b160aaf6685c45061e27ce5b8be6 --- /dev/null +++ b/changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml @@ -0,0 +1,4 @@ +--- +title: Expire related caches after changing HEAD +merge_request: +author: Minqi Pan diff --git a/changelogs/unreleased/post_receive-any-email.yml b/changelogs/unreleased/post_receive-any-email.yml deleted file mode 100644 index 3710b1b4b462ade60195044f66d971dbe1313917..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/post_receive-any-email.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "post_receive: accept any user email from last commit" -merge_request: 7225 -author: Elan Ruusamäe diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml deleted file mode 100644 index 26aabd9b647d321512b278284e77d73b1cca98e2..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/process-commit-worker-migration-encoding.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors -merge_request: -author: diff --git a/changelogs/unreleased/public-tags-api.yml b/changelogs/unreleased/public-tags-api.yml deleted file mode 100644 index f5e844470b2bf222ce8709855cd00e6af6543c81..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/public-tags-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow public access to some Tag API endpoints -merge_request: -author: diff --git a/changelogs/unreleased/re-style-issue-new-branch.yml b/changelogs/unreleased/re-style-issue-new-branch.yml new file mode 100644 index 0000000000000000000000000000000000000000..977a54ff2aedaeabe04a49675451baa5ca7ef49e --- /dev/null +++ b/changelogs/unreleased/re-style-issue-new-branch.yml @@ -0,0 +1,3 @@ +--- +title: Remove checking branches state in issue new branch button +merge_request: 8023 diff --git a/changelogs/unreleased/readme-link-fix.yml b/changelogs/unreleased/readme-link-fix.yml deleted file mode 100644 index 211d3b80c3ad8ab1ad18a6131560bf2492ad0307..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/readme-link-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken README.md UX guide link. -merge_request: -author: diff --git a/changelogs/unreleased/recaptcha_500.yml b/changelogs/unreleased/recaptcha_500.yml new file mode 100644 index 0000000000000000000000000000000000000000..de9ef183d5e217ef2d296df8cb4d2c51e2f42be1 --- /dev/null +++ b/changelogs/unreleased/recaptcha_500.yml @@ -0,0 +1,4 @@ +--- +title: Properly handle failed reCAPTCHA on user registration +merge_request: 8403 +author: diff --git a/changelogs/unreleased/reduce-queries-milestone-index.yml b/changelogs/unreleased/reduce-queries-milestone-index.yml new file mode 100644 index 0000000000000000000000000000000000000000..a779b58c973a8eda2a6bd6da37e3efbd23b43398 --- /dev/null +++ b/changelogs/unreleased/reduce-queries-milestone-index.yml @@ -0,0 +1,4 @@ +--- +title: Use cached values to compute total issues count in milestone index pages +merge_request: 8518 +author: diff --git a/changelogs/unreleased/refactor-create-service-spec.yml b/changelogs/unreleased/refactor-create-service-spec.yml deleted file mode 100644 index 148a0fee02c34010180b577de6fb4a116d8fd4d3..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/refactor-create-service-spec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor create service spec -merge_request: 7609 -author: Semyon Pupkov diff --git a/changelogs/unreleased/refresh-authorizations-tighter-lease.yml b/changelogs/unreleased/refresh-authorizations-tighter-lease.yml new file mode 100644 index 0000000000000000000000000000000000000000..ab42b2eb72dcd63d703c1d89fae40cbf94fecddc --- /dev/null +++ b/changelogs/unreleased/refresh-authorizations-tighter-lease.yml @@ -0,0 +1,4 @@ +--- +title: Synchronize all project authorization refreshing work to prevent race conditions +merge_request: +author: diff --git a/changelogs/unreleased/remove-backup-strategies.yml b/changelogs/unreleased/remove-backup-strategies.yml deleted file mode 100644 index 9f034613c2cef78e2925b616be6af9f8d9e14681..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/remove-backup-strategies.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Stop supporting Google and Azure as backup strategies -merge_request: -author: diff --git a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml deleted file mode 100644 index 6e02998b3a87221193e00dbbd313c49ff0a2d88b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Changed eslint airbnb config to the base airbnb config and corrected eslintrc - plugins and envs -merge_request: 7470 -author: Luke "Jared" Bennett diff --git a/changelogs/unreleased/remove-project-authorizations-id-column.yml b/changelogs/unreleased/remove-project-authorizations-id-column.yml new file mode 100644 index 0000000000000000000000000000000000000000..24c86f0fb1bd5ac99d917579de1bb6395c499177 --- /dev/null +++ b/changelogs/unreleased/remove-project-authorizations-id-column.yml @@ -0,0 +1,4 @@ +--- +title: Remove the project_authorizations.id column +merge_request: +author: diff --git a/changelogs/unreleased/remove-require-from-services.yml b/changelogs/unreleased/remove-require-from-services.yml deleted file mode 100644 index 400512e0314d53139ae26ef37e4bfb32be420f86..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/remove-require-from-services.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Remove unnecessary require_relative calls from service classes' -merge_request: '7601' -author: Semyon Pupkov diff --git a/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml b/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml new file mode 100644 index 0000000000000000000000000000000000000000..47d484e5c84fadff6a2da0125eb715c2e3d3ef51 --- /dev/null +++ b/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml @@ -0,0 +1,4 @@ +--- +title: Make successful pipeline emails off for watchers +merge_request: 8176 +author: diff --git a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml deleted file mode 100644 index bef11c636751b9ceed74cd8e7384de616f99a527..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary self from user model -merge_request: 7551 -author: Semyon Pupkov diff --git a/changelogs/unreleased/removing_unnecessary_indexes.yml b/changelogs/unreleased/removing_unnecessary_indexes.yml deleted file mode 100644 index 01314ab558562c4ba37a0c1de8b096e1a8d890d3..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/removing_unnecessary_indexes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary database indices -merge_request: -author: diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml deleted file mode 100644 index 827b0dbb1d3068c077be87f90c4589ce4fa1e846..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Render SVG images in diffs and notes -merge_request: 7747 -author: andrebsguedes diff --git a/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml new file mode 100644 index 0000000000000000000000000000000000000000..8ec3cfdbb089962050739a28b23465d427b1e7ec --- /dev/null +++ b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml @@ -0,0 +1,4 @@ +--- +title: Restore backup correctly when "BACKUP" environment variable is passed +merge_request: 8477 +author: diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml deleted file mode 100644 index 46a3e459fefad7890141ceeb9d5a0b1d81ff53ab..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/right-sidebar-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for right_sidebar_spec -merge_request: 7687 -author: winniehell diff --git a/changelogs/unreleased/rs-project-team-helpers.yml b/changelogs/unreleased/rs-project-team-helpers.yml deleted file mode 100644 index 79abcbce1e37eb645e479f5af6731eeebf970741..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/rs-project-team-helpers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add shortcuts for adding users to a project team with a specific role -merge_request: -author: Nikolay Ponomarev and Dino M diff --git a/changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml b/changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml new file mode 100644 index 0000000000000000000000000000000000000000..7107ddfd982d8c4ac29058176cc37074f0c09f6d --- /dev/null +++ b/changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml @@ -0,0 +1,4 @@ +--- +title: Ensure updating project settings shows a flash message on success +merge_request: 8579 +author: Sandish Chen diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml deleted file mode 100644 index 88945600886e31d9058400669cdc9c88cb9c167b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/shortcuts-issuable-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for shortcuts_issuable_spec -merge_request: 7685 -author: winniehell diff --git a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml deleted file mode 100644 index ca11e3b94a7bea43c974218510f143121ad841a1..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Simplify copy on "Create a new list" dropdown in Issue Boards -merge_request: 7605 -author: Victor Rodrigues diff --git a/changelogs/unreleased/single-edit-comment-widget-2.yml b/changelogs/unreleased/single-edit-comment-widget-2.yml new file mode 100644 index 0000000000000000000000000000000000000000..e8b8beb15dee7b09487dcba2596f9eeba66c6e27 --- /dev/null +++ b/changelogs/unreleased/single-edit-comment-widget-2.yml @@ -0,0 +1,5 @@ +--- +title: Refactored note edit form to improve frontend performance on MR and Issues + pages, especially pages with has a lot of discussions in it +merge_request: 8356 +author: diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml deleted file mode 100644 index 804bd05b613abe92d77ed9d2b461cfcb8ccb06a6..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/small-emoji-adjustments.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Various small emoji positioning adjustments -merge_request: -author: diff --git a/changelogs/unreleased/support-google-cloud-storage-backups.yml b/changelogs/unreleased/support-google-cloud-storage-backups.yml new file mode 100644 index 0000000000000000000000000000000000000000..cec279a5c736a588b90c9b5cfdd9b763893304f0 --- /dev/null +++ b/changelogs/unreleased/support-google-cloud-storage-backups.yml @@ -0,0 +1,4 @@ +--- +title: Re-add Google Cloud Storage as a backup strategy +merge_request: +author: diff --git a/changelogs/unreleased/switch-to-sassc.yml b/changelogs/unreleased/switch-to-sassc.yml new file mode 100644 index 0000000000000000000000000000000000000000..3e6c4baf6d9477f89fdfb7f162079ecc8b3a976b --- /dev/null +++ b/changelogs/unreleased/switch-to-sassc.yml @@ -0,0 +1,4 @@ +--- +title: Switch to sassc-rails for faster stylesheet compilation +merge_request: 8556 +author: Richard Macklin diff --git a/changelogs/unreleased/time-tracking-api.yml b/changelogs/unreleased/time-tracking-api.yml new file mode 100644 index 0000000000000000000000000000000000000000..b58d73bef81a14d3b60337a2d1384b7533074647 --- /dev/null +++ b/changelogs/unreleased/time-tracking-api.yml @@ -0,0 +1,4 @@ +--- +title: Add new endpoints for Time Tracking. +merge_request: 8483 +author: diff --git a/changelogs/unreleased/timeago-perf-fix.yml b/changelogs/unreleased/timeago-perf-fix.yml deleted file mode 100644 index 265e7db29a9218fcedb997a88aba13cee491c8f0..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/timeago-perf-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed timeago re-rendering every timeago -merge_request: -author: diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml deleted file mode 100644 index 755b0379a16bd2917b09c08e205450046ac4b6c0..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/unescape-relative-path.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Avoid escaping relative links in Markdown twice -merge_request: 7940 -author: winniehell diff --git a/changelogs/unreleased/update-api-spec-files.yml b/changelogs/unreleased/update-api-spec-files.yml deleted file mode 100644 index 349d866cf227f8b8570bf6ba5b9557c415a7ab5c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/update-api-spec-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update API spec files to describe the correct class -merge_request: -author: Livier diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml deleted file mode 100644 index ddb3c1c8da4f7c42f1e35743be85a3b8e2a8be13..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/update-button-font-weight.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Updates the font weight of button styles because of the change to system fonts -merge_request: -author: diff --git a/changelogs/unreleased/update-git-version-in-doc.yml b/changelogs/unreleased/update-git-version-in-doc.yml deleted file mode 100644 index cb3260f71cd7b6010b3f267381f96dc1c8697ed8..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/update-git-version-in-doc.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump Git version requirement to 2.8.4 -merge_request: -author: diff --git a/changelogs/unreleased/update-gitlab-markup-gem.yml b/changelogs/unreleased/update-gitlab-markup-gem.yml new file mode 100644 index 0000000000000000000000000000000000000000..96cdfd051f09a2ba91ba5524230289c8fd0ddd42 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-markup-gem.yml @@ -0,0 +1,4 @@ +--- +title: Update the gitlab-markup gem to the version 1.5.1 +merge_request: 8509 +author: diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml deleted file mode 100644 index e4395461560c6851918b0f7770fadc7bf760032c..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/use-st-commits-where-possible.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace references to MergeRequestDiff#commits with st_commits when we care - only about the number of commits -merge_request: 7668 -author: diff --git a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml deleted file mode 100644 index 3fb025806b01b7d7a21fca8e54a06086765836b2..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Validate state param when filtering issuables -merge_request: -author: diff --git a/changelogs/unreleased/validate-title-length.yml b/changelogs/unreleased/validate-title-length.yml new file mode 100644 index 0000000000000000000000000000000000000000..7abf1c4d05acb9d56129fba6d16ad3047911f6b1 --- /dev/null +++ b/changelogs/unreleased/validate-title-length.yml @@ -0,0 +1,4 @@ +--- +title: "Validate label's title length" +merge_request: 5767 +author: Tomáš Kukrál diff --git a/changelogs/unreleased/view-ce-vs-ee.yml b/changelogs/unreleased/view-ce-vs-ee.yml new file mode 100644 index 0000000000000000000000000000000000000000..38bce4ac7c3ff7e820917aa5e7c3aba99d020f51 --- /dev/null +++ b/changelogs/unreleased/view-ce-vs-ee.yml @@ -0,0 +1,4 @@ +--- +title: About GitLab link in sidebar that links to help page +merge_request: 8316 +author: diff --git a/changelogs/unreleased/zen-mode-fixture.yml b/changelogs/unreleased/zen-mode-fixture.yml deleted file mode 100644 index bec6f6e6dba73b85021028ee2930a64c50ca9320..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zen-mode-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for zen_mode_spec -merge_request: 7686 -author: winniehell diff --git a/changelogs/unreleased/zj-expose-coverage-pipelines.yml b/changelogs/unreleased/zj-expose-coverage-pipelines.yml deleted file mode 100644 index 34e4926e58a0ba8771f23792ce6795b44caced90..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zj-expose-coverage-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: expose pipeline coverage' -merge_request: -author: diff --git a/changelogs/unreleased/zj-fix-label-creation-non-members.yml b/changelogs/unreleased/zj-fix-label-creation-non-members.yml deleted file mode 100644 index ae4824f82fa8b6210b652b6f9bf41334fefad0da..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zj-fix-label-creation-non-members.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Non members cannot create labels through the API -merge_request: -author: diff --git a/changelogs/unreleased/zj-guest-reads-public-builds.yml b/changelogs/unreleased/zj-guest-reads-public-builds.yml deleted file mode 100644 index 1859addd606c46bab7383e10af7727d14d83cce8..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zj-guest-reads-public-builds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Guests can read builds when public -merge_request: 6842 -author: diff --git a/changelogs/unreleased/zj-issue-new-over-issue-create.yml b/changelogs/unreleased/zj-issue-new-over-issue-create.yml deleted file mode 100644 index 9dd463e4efaf6d6c44071fec092cd8373403fb73..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zj-issue-new-over-issue-create.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Accept issue new as command to create an issue -merge_request: -author: diff --git a/changelogs/unreleased/zj-issue-search-slash-command.yml b/changelogs/unreleased/zj-issue-search-slash-command.yml deleted file mode 100644 index de41c39d545b4be9347e4673be12b8732a278599..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zj-issue-search-slash-command.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add issue search slash command -merge_request: -author: diff --git a/changelogs/unreleased/zj-unadressable-url-variables.yml b/changelogs/unreleased/zj-unadressable-url-variables.yml new file mode 100644 index 0000000000000000000000000000000000000000..6c412bd05400b54e730c3ee0af6bf060b1f93734 --- /dev/null +++ b/changelogs/unreleased/zj-unadressable-url-variables.yml @@ -0,0 +1,4 @@ +--- +title: Don't validate environment urls on .gitlab-ci.yml +merge_request: +author: diff --git a/changelogs/unreleased/zj-use-ruby-2-3-3.yml b/changelogs/unreleased/zj-use-ruby-2-3-3.yml deleted file mode 100644 index 0d1a0fcd79d20d51aad91be4c45a078f27d156ad..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/zj-use-ruby-2-3-3.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump ruby version to 2.3.3 -merge_request: 7904 -author: diff --git a/config/application.rb b/config/application.rb index 0aa2873f94a4bed120f9c8a7b5c8c198c7d847a5..f00e58a36ca89d354bed258f01771a0f3c4773aa 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,7 @@ module Gitlab # # Parameters filtered: # - Password (:password, :password_confirmation) - # - Private tokens (:private_token, :authentication_token) + # - Private tokens # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build variables (:variables) @@ -60,11 +60,13 @@ module Gitlab encrypted_key hook import_url + incoming_email_token key otp_attempt password password_confirmation private_token + runners_token secret_token sentry_dsn variables @@ -81,16 +83,23 @@ module Gitlab # Enable the asset pipeline config.assets.enabled = true config.assets.paths << Gemojione.images_path + config.assets.paths << "vendor/assets/fonts" config.assets.precompile << "*.png" config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "lib/vue_resource.js" + config.assets.precompile << "katex.css" + config.assets.precompile << "katex.js" + config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" + config.assets.precompile << "merge_request_widget/ci_bundle.js" + config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" @@ -98,9 +107,13 @@ module Gitlab config.assets.precompile << "environments/environments_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" + config.assets.precompile << "terminal/terminal_bundle.js" + config.assets.precompile << "filtered_search/filtered_search_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" + config.assets.precompile << "vue_pipelines_index/index.js" + config.assets.precompile << "vendor/assets/fonts/*" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 327e4a7937c01fc542d95645ad02cf5b9084d5d1..42e5f105d4659ba8814e3dd21784f18a210b0be0 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -153,6 +153,12 @@ production: &base # The location where LFS objects are stored (default: shared/lfs-objects). # storage_path: shared/lfs-objects + ## Mattermost + ## For enabling Add to Mattermost button + mattermost: + enabled: false + host: 'https://mattermost.example.com' + ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: @@ -362,6 +368,16 @@ production: &base # login_url: '/cas/login', # service_validate_url: '/cas/p3/serviceValidate', # logout_url: '/cas/logout'} } + # - { name: 'authentiq', + # # for client credentials (client ID and secret), go to https://www.authentiq.com/ + # app_id: 'YOUR_CLIENT_ID', + # app_secret: 'YOUR_CLIENT_SECRET', + # args: { + # scope: 'aq:name email~rs address aq:push' + # # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost' + # # redirect_uri: 'YOUR_REDIRECT_URI' + # } + # } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', @@ -570,4 +586,4 @@ test: admin_group: '' staging: - <<: *base + <<: *base \ No newline at end of file diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0ee1b1ec6344eca9855c26b4fbf811deab222bcb..ee97b4e42b95197e30716eb90d9614d08d6737d7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -213,7 +213,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['domain_whitelist'] ||= [] -Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project] +Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea] Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) @@ -261,6 +261,13 @@ Settings['lfs'] ||= Settingslogic.new({}) Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil? Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root) +# +# Mattermost +# +Settings['mattermost'] ||= Settingslogic.new({}) +Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil? +Settings.mattermost['host'] = nil unless Settings.mattermost.enabled + # # Gravatar # diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index fc4b0a72addf4190b96ce3e38a2dfc143dcff239..88cd0f5f6524dffc586a6e04ac5ad2907e7f6463 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -52,8 +52,8 @@ Doorkeeper.configure do # Define access token scopes for your provider # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes - default_scopes :api - # optional_scopes :write, :update + default_scopes(*Gitlab::Auth::DEFAULT_SCOPES) + optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES) # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 3d1a41a46525d29327b2f7f0db903fb5a6794acf..d4197da3fa9b9a51c81bb915538ea56af2c36ef6 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -10,5 +10,5 @@ # end # ActiveSupport::Inflector.inflections do |inflect| - inflect.uncountable %w(award_emoji) + inflect.uncountable %w(award_emoji project_statistics) end diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a3388a267e83e11abceed020018131ce80e4cef --- /dev/null +++ b/config/initializers/math_lexer.rb @@ -0,0 +1,2 @@ +# Touch the lexers so it is registered with Rouge +Rouge::Lexers::Math diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 26c30e523a765b939bba432f43148723c89df0ba..ab5a0561b8c6d71a2334ba121bc8e782587f6677 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -26,3 +26,9 @@ if Gitlab.config.omniauth.enabled end end end + +module OmniAuth + module Strategies + autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket') + end +end diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb deleted file mode 100644 index e4f09a2d02058d36952d667b866e2ff603942d42..0000000000000000000000000000000000000000 --- a/config/initializers/public_key.rb +++ /dev/null @@ -1,2 +0,0 @@ -path = File.expand_path("~/.ssh/bitbucket_rsa.pub") -Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 1d7a3f03ace85b9c0d89b995701ef783f7cf84ed..5a7365bb0f6fd64a7d55bf65fbb7300ae87fd116 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -34,7 +34,7 @@ Sidekiq.configure_server do |config| # Database pool should be at least `sidekiq_concurrency` + 2 # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md config = ActiveRecord::Base.configurations[Rails.env] || - Rails.application.config.database_configuration[Rails.env] + Rails.application.config.database_configuration[Rails.env] config['pool'] = Sidekiq.options[:concurrency] + 2 ActiveRecord::Base.establish_connection(config) Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb new file mode 100644 index 0000000000000000000000000000000000000000..84d809741c47d1c88a7afdf0c6f4d239b72933c6 --- /dev/null +++ b/config/initializers/workhorse_multipart.rb @@ -0,0 +1,25 @@ +Rails.application.configure do |config| + config.middleware.use(Gitlab::Middleware::Multipart) +end + +# The Gitlab::Middleware::Multipart middleware inserts instances of our +# own ::UploadedFile class in the Rack env of requests. These instances +# will be blocked by the 'strong parameters' feature of ActionController +# unless we somehow whitelist them. At the moment it seems the only way +# to do that is by monkey-patching. +# +module Gitlab + module StrongParameterScalars + GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile] + + def permitted_scalar?(value) + super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } + end + end +end + +module ActionController + class Parameters + prepend Gitlab::StrongParameterScalars + end +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index a4032a2142092d837a4fb42dac67a66d081891ec..1d728282d90fcbf17d20ca88870a6a6903a55d99 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -59,6 +59,7 @@ en: unknown: "The access token is invalid" scopes: api: Access your API + read_user: Read user information flash: applications: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5ae985da56107ab0ad465af2710b763a771b7849..8e99239f350d6ec40cf6dea1837690c6bd9c6bfe 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -6,7 +6,6 @@ namespace :admin do member do get :projects get :keys - get :groups put :block put :unblock put :unlock @@ -28,9 +27,19 @@ namespace :admin do resources :applications - resources :groups, constraints: { id: /[^\/]+/ } do - member do + resources :groups, only: [:index, :new, :create] + + scope(path: 'groups/*id', + controller: :groups, + constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do + + scope(as: :group) do put :members_update + get :edit, action: :edit + get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy end end @@ -50,14 +59,13 @@ namespace :admin do resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } - resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do - root to: 'projects#index', as: :projects + resources :projects, only: [:index] + scope(path: 'projects/*namespace_id', as: :namespace) do resources(:projects, path: '/', - constraints: { id: /[a-zA-Z.0-9_\-]+/ }, - only: [:index, :show]) do - root to: 'projects#show' + constraints: { id: Gitlab::Regex.project_route_regex }, + only: [:show]) do member do put :transfer diff --git a/config/routes/import.rb b/config/routes/import.rb index 89f3b3f6378019cd827d1607d3c19e6bfbea6cca..c378253bf157bf126be8af47ebecfc004953c957 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -6,6 +6,12 @@ namespace :import do get :jobs end + resource :gitea, only: [:create, :new], controller: :gitea do + post :personal_access_token + get :status + get :jobs + end + resource :gitlab, only: [:create], controller: :gitlab do get :status get :callback diff --git a/config/routes/project.rb b/config/routes/project.rb index 0754f0ec3b0a1cca0a6b98a6cc2d820f834fd763..1fc6ed28c74f73206e8c19092b23b6b9e8cbaef4 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -11,6 +11,18 @@ constraints(ProjectUrlConstrainer.new) do module: :projects, as: :project) do + resources :autocomplete_sources, only: [] do + collection do + get 'emojis' + get 'members' + get 'issues' + get 'merge_requests' + get 'labels' + get 'milestones' + get 'commands' + end + end + # # Templates # @@ -20,10 +32,7 @@ constraints(ProjectUrlConstrainer.new) do resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do member do get :branches - get :builds get :pipelines - post :cancel_builds - post :retry_builds post :revert post :cherry_pick get :diff_for_path @@ -64,6 +73,8 @@ constraints(ProjectUrlConstrainer.new) do end end + resource :mattermost, only: [:new, :create] + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do put :enable @@ -80,10 +91,10 @@ constraints(ProjectUrlConstrainer.new) do get :diffs get :conflicts get :conflict_for_path - get :builds get :pipelines get :merge_check post :merge + get :merge_widget_refresh post :cancel_merge_when_build_succeeds get :ci_status get :ci_environments_status @@ -127,6 +138,7 @@ constraints(ProjectUrlConstrainer.new) do end member do + get :stage post :cancel post :retry get :builds @@ -136,6 +148,8 @@ constraints(ProjectUrlConstrainer.new) do resources :environments, except: [:destroy] do member do post :stop + get :terminal + get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } end end @@ -294,6 +308,10 @@ constraints(ProjectUrlConstrainer.new) do end end + namespace :settings do + resource :members, only: [:show] + end + # Since both wiki and repository routing contains wildcard characters # its preferable to keep it below all other project routes draw :wiki @@ -316,7 +334,6 @@ constraints(ProjectUrlConstrainer.new) do post :remove_export post :generate_new_export get :download_export - get :autocomplete_sources get :activity get :refs put :new_issue_address diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 69136b73946803e1e236596b2eb62d60b0f75323..022b0e809172b279741eb5804ed43d8a17093e43 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -29,6 +29,7 @@ - [email_receiver, 2] - [emails_on_push, 2] - [mailers, 2] + - [use_key, 1] - [repository_fork, 1] - [repository_import, 1] - [project_service, 1] @@ -46,5 +47,6 @@ - [repository_check, 1] - [system_hook, 1] - [git_garbage_collect, 1] + - [reactive_caching, 1] - [cronjob, 1] - [default, 1] diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 08ad3097d343a26852412ca475e1bd02f7306ae3..be95d788850b9506db0efd49d891ef67440b45db 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -1,26 +1,50 @@ class Gitlab::Seeder::Pipelines STAGES = %w[build test deploy notify] BUILDS = [ - { name: 'build:linux', stage: 'build', status: :success }, - { name: 'build:osx', stage: 'build', status: :success }, - { name: 'rspec:linux 0 3', stage: 'test', status: :success }, - { name: 'rspec:linux 1 3', stage: 'test', status: :success }, - { name: 'rspec:linux 2 3', stage: 'test', status: :success }, - { name: 'rspec:windows 0 3', stage: 'test', status: :success }, - { name: 'rspec:windows 1 3', stage: 'test', status: :success }, - { name: 'rspec:windows 2 3', stage: 'test', status: :success }, - { name: 'rspec:windows 2 3', stage: 'test', status: :success }, - { name: 'rspec:osx', stage: 'test', status_event: :success }, - { name: 'spinach:linux', stage: 'test', status: :success }, - { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true}, - { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending }, - { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, - { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled }, - { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } }, - { name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped }, - { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped }, + # build stage + { name: 'build:linux', stage: 'build', status: :success, + queued_at: 10.hour.ago, started_at: 9.hour.ago, finished_at: 8.hour.ago }, + { name: 'build:osx', stage: 'build', status: :success, + queued_at: 10.hour.ago, started_at: 10.hour.ago, finished_at: 9.hour.ago }, + + # test stage + { name: 'rspec:linux 0 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:linux 1 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:linux 2 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:windows 0 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:windows 1 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:windows 2 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:windows 2 3', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'rspec:osx', stage: 'test', status_event: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'spinach:linux', stage: 'test', status: :success, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true, + queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, + + # deploy stage + { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, + options: { environment: { action: 'start', on_stop: 'stop staging' } }, + queued_at: 7.hour.ago, started_at: 6.hour.ago, finished_at: 4.hour.ago }, + { name: 'stop staging', stage: 'deploy', environment: 'staging', + when: 'manual', status: :skipped }, + { name: 'production', stage: 'deploy', environment: 'production', + when: 'manual', status: :skipped }, + + # notify stage { name: 'slack', stage: 'notify', when: 'manual', status: :created }, ] + EXTERNAL_JOBS = [ + { name: 'jenkins', stage: 'test', status: :success, + queued_at: 7.hour.ago, started_at: 6.hour.ago, finished_at: 4.hour.ago }, + ] def initialize(project) @project = project @@ -30,11 +54,12 @@ class Gitlab::Seeder::Pipelines pipelines.each do |pipeline| begin BUILDS.each { |opts| build_create!(pipeline, opts) } - commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success) + EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) } print '.' rescue ActiveRecord::RecordInvalid print 'F' ensure + pipeline.update_duration pipeline.update_status end end @@ -115,7 +140,7 @@ class Gitlab::Seeder::Pipelines def job_attributes(pipeline, opts) { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), - ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, + ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline, created_at: Time.now, updated_at: Time.now }.merge(opts) end diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..5522f31629ac65e84305b76c50d9e7a23073a3fb --- /dev/null +++ b/db/fixtures/production/010_settings.rb @@ -0,0 +1,16 @@ +if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + + if settings.save + puts "Saved Runner Registration Token".color(:green) + else + puts "Could not save Runner Registration Token".color(:red) + puts + settings.errors.full_messages.map do |message| + puts "--> #{message}".color(:red) + end + puts + exit 1 + end +end diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 8cb120f7007b358f02ed253c9111d25112730481..561184615cc6ce4def4c2170d5d641291995a6c1 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -1,5 +1,9 @@ # rubocop:disable all class MoveSlackServiceToWebhook < ActiveRecord::Migration + + DOWNTIME = true + DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"' + def change SlackService.all.each do |slack_service| if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } diff --git a/db/migrate/20160811172945_add_can_push_to_keys.rb b/db/migrate/20160811172945_add_can_push_to_keys.rb new file mode 100644 index 0000000000000000000000000000000000000000..5fd303fe8fbdfd2fd5d499721fb78ff49f9acbcc --- /dev/null +++ b/db/migrate/20160811172945_add_can_push_to_keys.rb @@ -0,0 +1,14 @@ +class AddCanPushToKeys < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:keys, :can_push, :boolean, default: false, allow_null: false) + end + + def down + remove_column(:keys, :can_push) + end +end diff --git a/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb new file mode 100644 index 0000000000000000000000000000000000000000..91479de840b779f24c73cbaa9c67334c8458daba --- /dev/null +++ b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb @@ -0,0 +1,19 @@ +# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. +# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to +# `[]`. + +class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml + end + + def down + remove_column :personal_access_tokens, :scopes + end +end diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb index 696914f8e4dca6a0fb0837d1c19339530c3bd65d..29040583aa25367eb1788ee270777570ad62d252 100644 --- a/db/migrate/20161117114805_remove_undeleted_groups.rb +++ b/db/migrate/20161117114805_remove_undeleted_groups.rb @@ -5,47 +5,87 @@ class RemoveUndeletedGroups < ActiveRecord::Migration DOWNTIME = false def up + is_ee = defined?(Gitlab::License) + + if is_ee + execute <<-EOF.strip_heredoc + DELETE FROM path_locks + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM remote_mirrors + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ); + EOF + end + execute <<-EOF.strip_heredoc - DELETE FROM projects - WHERE namespace_id IN ( - SELECT id FROM ( - SELECT id - FROM namespaces - WHERE deleted_at IS NOT NULL - ) namespace_ids + DELETE FROM lists + WHERE label_id IN ( + SELECT id + FROM labels + WHERE group_id IN (#{namespaces_pending_removal}) + ); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM lists + WHERE board_id IN ( + SELECT id + FROM boards + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ) ); EOF - if defined?(Gitlab::License) + execute <<-EOF.strip_heredoc + DELETE FROM labels + WHERE group_id IN (#{namespaces_pending_removal}); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM boards + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ) + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}); + EOF + + if is_ee # EE adds these columns but we have to make sure this data is cleaned up # here before we run the DELETE below. An alternative would be patching # this migration in EE but this will only result in a mess and confusing # migrations. execute <<-EOF.strip_heredoc DELETE FROM protected_branch_push_access_levels - WHERE group_id IN ( - SELECT id FROM ( - SELECT id - FROM namespaces - WHERE deleted_at IS NOT NULL - ) namespace_ids - ); + WHERE group_id IN (#{namespaces_pending_removal}); EOF execute <<-EOF.strip_heredoc DELETE FROM protected_branch_merge_access_levels - WHERE group_id IN ( - SELECT id FROM ( - SELECT id - FROM namespaces - WHERE deleted_at IS NOT NULL - ) namespace_ids - ); + WHERE group_id IN (#{namespaces_pending_removal}); EOF end - # This removes namespaces that were supposed to be soft deleted but still - # reside in the database. + # This removes namespaces that were supposed to be deleted but still reside + # in the database. execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;" end @@ -54,4 +94,12 @@ class RemoveUndeletedGroups < ActiveRecord::Migration # If someone is trying to rollback for other reasons, we should not throw an Exception. # raise ActiveRecord::IrreversibleMigration end + + def namespaces_pending_removal + "SELECT id FROM ( + SELECT id + FROM namespaces + WHERE deleted_at IS NOT NULL + ) namespace_ids" + end end diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb index 6754e58300052bdc9165b16cace5e6c454f68e02..c3536d6d911a006d88e2a8df0377593b2a02f1bf 100644 --- a/db/migrate/20161130095245_fill_routes_table.rb +++ b/db/migrate/20161130095245_fill_routes_table.rb @@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration end def down - Route.delete_all(source_type: 'Namespace') + execute("DELETE FROM routes WHERE source_type = 'Namespace'") end end diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb index 14700583be55a409237e8b924e2786f429dcbaa5..56ba6fcdbe35cf1c2a58e5be5e7ead3f7d908a26 100644 --- a/db/migrate/20161130101252_fill_projects_routes_table.rb +++ b/db/migrate/20161130101252_fill_projects_routes_table.rb @@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration DOWNTIME_REASON = 'No new projects should be created during data copy' def up - execute <<-EOF - INSERT INTO routes - (source_id, source_type, path) - (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects - INNER JOIN namespaces ON projects.namespace_id = namespaces.id) - EOF + if Gitlab::Database.postgresql? + execute <<-EOF + INSERT INTO routes (source_id, source_type, path) + (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path) + FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id + ORDER BY namespaces.path, projects.path, projects.id DESC) + EOF + else + execute <<-EOF + INSERT INTO routes (source_id, source_type, path) + (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) + FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id) + EOF + end end def down - Route.delete_all(source_type: 'Project') + execute("DELETE FROM routes WHERE source_type = 'Project'") end end diff --git a/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..b8d8742ae402bd50539475de46beb1871db69bdd --- /dev/null +++ b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPlantUmlUrlToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :plantuml_url, :string + end +end diff --git a/db/migrate/20161201155511_create_project_statistics.rb b/db/migrate/20161201155511_create_project_statistics.rb new file mode 100644 index 0000000000000000000000000000000000000000..26e6d3623eb1317526fc2a5ae504010e411ba81a --- /dev/null +++ b/db/migrate/20161201155511_create_project_statistics.rb @@ -0,0 +1,20 @@ +class CreateProjectStatistics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + # use bigint columns to support values >2GB + counter_column = { limit: 8, null: false, default: 0 } + + create_table :project_statistics do |t| + t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + t.references :namespace, null: false, index: true + t.integer :commit_count, counter_column + t.integer :storage_size, counter_column + t.integer :repository_size, counter_column + t.integer :lfs_objects_size, counter_column + t.integer :build_artifacts_size, counter_column + end + end +end diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb new file mode 100644 index 0000000000000000000000000000000000000000..3ae3f2c159b57c9c09e74ae6db2cbe5693650ce4 --- /dev/null +++ b/db/migrate/20161201160452_migrate_project_statistics.rb @@ -0,0 +1,23 @@ +class MigrateProjectStatistics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Removes two columns from the projects table' + + def up + # convert repository_size in float (megabytes) to integer (bytes), + # initialize total storage_size with repository_size + execute <<-EOF + INSERT INTO project_statistics (project_id, namespace_id, commit_count, storage_size, repository_size) + SELECT id, namespace_id, commit_count, (repository_size * 1024 * 1024), (repository_size * 1024 * 1024) FROM projects + EOF + + remove_column :projects, :repository_size + remove_column :projects, :commit_count + end + + def down + add_column_with_default :projects, :repository_size, :float, default: 0.0 + add_column_with_default :projects, :commit_count, :integer, default: 0 + end +end diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb index 510796e05f26e7040d359d47a78314c626cafb1d..d73b08475064dc444ea0da78d5ab2ecd4e508805 100644 --- a/db/migrate/20161202152031_remove_duplicates_from_routes.rb +++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb @@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration DOWNTIME = false def up - select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| - path = connection.quote(row['path']) - execute(%Q{ - DELETE FROM #{quote_table_name(:routes)} - WHERE path = #{path} - AND id != ( - SELECT id FROM ( - SELECT max(id) AS id - FROM #{quote_table_name(:routes)} - WHERE path = #{path} - ) max_ids - ) - }) - end + # We can skip this migration when running a PostgreSQL database because + # we use an optimized query in the "FillProjectsRoutesTable" migration + # to fill these values that avoid duplicate entries in the routes table. + return unless Gitlab::Database.mysql? + + execute <<-EOF + DELETE duplicated_rows.* + FROM routes AS duplicated_rows + INNER JOIN ( + SELECT path, MAX(id) as max_id + FROM routes + GROUP BY path + HAVING COUNT(*) > 1 + ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id; + EOF end def down diff --git a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..3677f978cc2cfada199b9cf6142593c42c6dc5c6 --- /dev/null +++ b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :plantuml_enabled, :boolean + end +end diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb new file mode 100644 index 0000000000000000000000000000000000000000..2977917f2d10585c2a1e0e68053d1d92f1c44a89 --- /dev/null +++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + constraint_name = 'namespaces_path_key' + + transaction do + if index_exists?(:namespaces, :path) + remove_index(:namespaces, :path) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};") + end + end + end + + def down + unless index_exists?(:namespaces, :path) + add_concurrent_index(:namespaces, :path, unique: true) + end + end + + def constraint_exists?(name) + indexes(:namespaces).map(&:name).include?(name) + end +end diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb new file mode 100644 index 0000000000000000000000000000000000000000..b0bac7d121e3763be9e104511b205c9265777db1 --- /dev/null +++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPathIndexToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_concurrent_index :namespaces, :path + end + + def down + if index_exists?(:namespaces, :path) + remove_index :namespaces, :path + end + end +end diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb new file mode 100644 index 0000000000000000000000000000000000000000..cc9d4974baaffb85f24ac9871bd042c4f66bd1d0 --- /dev/null +++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb @@ -0,0 +1,36 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + constraint_name = 'namespaces_name_key' + + transaction do + if index_exists?(:namespaces, :name) + remove_index(:namespaces, :name) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};") + end + end + end + + def down + unless index_exists?(:namespaces, :name) + add_concurrent_index(:namespaces, :name, unique: true) + end + end + + def constraint_exists?(name) + indexes(:namespaces).map(&:name).include?(name) + end +end diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb new file mode 100644 index 0000000000000000000000000000000000000000..b3f3cb68a99d72f64da87627d95c534e74e9d024 --- /dev/null +++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddNameIndexToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_concurrent_index(:namespaces, [:name, :parent_id], unique: true) + end + + def down + if index_exists?(:namespaces, [:name, :parent_id]) + remove_index :namespaces, [:name, :parent_id] + end + end +end diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb new file mode 100644 index 0000000000000000000000000000000000000000..b74552e762dcd1657cb335ba50d148074305a529 --- /dev/null +++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb @@ -0,0 +1,53 @@ +class FixupEnvironmentNameUniqueness < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Renaming non-unique environments' + + def up + environments = Arel::Table.new(:environments) + + # Get all [project_id, name] pairs that occur more than once + finder_sql = environments. + group(environments[:project_id], environments[:name]). + having(Arel.sql("COUNT(1)").gt(1)). + project(environments[:project_id], environments[:name]). + to_sql + + conflicting = connection.exec_query(finder_sql) + + conflicting.rows.each do |project_id, name| + fix_duplicates(project_id, name) + end + end + + def down + # Nothing to do + end + + # Rename conflicting environments by appending "-#{id}" to all but the first + def fix_duplicates(project_id, name) + environments = Arel::Table.new(:environments) + finder_sql = environments. + where(environments[:project_id].eq(project_id)). + where(environments[:name].eq(name)). + order(environments[:id].asc). + project(environments[:id], environments[:name]). + to_sql + + # Now we have the data for all the conflicting rows + conflicts = connection.exec_query(finder_sql).rows + conflicts.shift # Leave the first row alone + + conflicts.each do |id, name| + update_sql = + Arel::UpdateManager.new(ActiveRecord::Base). + table(environments). + set(environments[:name] => name + "-" + id.to_s). + where(environments[:id].eq(id)). + to_sql + + connection.exec_update(update_sql, self.class.name, []) + end + end +end diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac680c8d10f5334afbf8e45ed0cd64294891e5b9 --- /dev/null +++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb @@ -0,0 +1,18 @@ +class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = true + DOWNTIME_REASON = 'Making a non-unique index into a unique index' + + def up + remove_index :environments, [:project_id, :name] + add_concurrent_index :environments, [:project_id, :name], unique: true + end + + def down + remove_index :environments, [:project_id, :name], unique: true + add_concurrent_index :environments, [:project_id, :name] + end +end diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb new file mode 100644 index 0000000000000000000000000000000000000000..7153e6a32b1cdc93088899852d3c17824cefa8ba --- /dev/null +++ b/db/migrate/20161207231626_add_environment_slug.rb @@ -0,0 +1,60 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data' + + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + + def up + environments = Arel::Table.new(:environments) + + add_column :environments, :slug, :string + finder = environments.project(:id, :name) + + connection.exec_query(finder.to_sql).rows.each do |id, name| + updater = Arel::UpdateManager.new(ActiveRecord::Base). + table(environments). + set(environments[:slug] => generate_slug(name)). + where(environments[:id].eq(id)) + + connection.exec_update(updater.to_sql, self.class.name, []) + end + + change_column_null :environments, :slug, false + end + + def down + remove_column :environments, :slug + end + + # Copy of the Environment#generate_slug implementation + def generate_slug(name) + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + slugified + end + + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end +end diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb new file mode 100644 index 0000000000000000000000000000000000000000..e9fcef1cd45601d53f132d211c7a743a3d2cdbcd --- /dev/null +++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Adding a *unique* index to environments.slug' + + disable_ddl_transaction! + + def change + add_concurrent_index :environments, [:project_id, :slug], unique: true + end +end diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..dc38d0ac90674cffe50429ebb16612006683305d --- /dev/null +++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb @@ -0,0 +1,11 @@ +class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # This migration is a no-op, as it existed in an RC but we renamed + # SlackNotificationService back to SlackService: + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8191#note_20310845 + def change + end +end diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb new file mode 100644 index 0000000000000000000000000000000000000000..241afc6b097a415fd1efea879920b8fd7d392723 --- /dev/null +++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb @@ -0,0 +1,82 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDotGitFromGroupNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + invalid_groups.each do |group| + path_was = group['path'] + path_was_wildcard = quote_string("#{path_was}/%") + path = quote_string(rename_path(path_was)) + + move_namespace(group['id'], path_was, path) + + execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}" + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}" + + select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route| + new_path = "#{path}/#{route['path'].split('/').last}" + execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}" + end + end + end + + def down + # nothing to do here + end + + private + + def invalid_groups + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'") + end + + def route_exists?(path) + select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? + end + + # Accepts invalid path like test.git and returns test_git or + # test_git1 if test_git already taken + def rename_path(path) + # To stay closer with original name and reduce risk of duplicates + # we rename suffix instead of removing it + path = path.sub(/\.git\z/, '_git') + + counter = 0 + base = path + + while route_exists?(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + + def move_namespace(group_id, path_was, path) + repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| + Gitlab.config.repositories.storages[row['repository_storage']] + end.compact + + # Move the namespace directory in all storages paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + end +end diff --git a/db/migrate/20161221152132_add_last_used_at_to_key.rb b/db/migrate/20161221152132_add_last_used_at_to_key.rb new file mode 100644 index 0000000000000000000000000000000000000000..fb2b15817de096b926737c547f41d9d8c667e313 --- /dev/null +++ b/db/migrate/20161221152132_add_last_used_at_to_key.rb @@ -0,0 +1,9 @@ +class AddLastUsedAtToKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :keys, :last_used_at, :datetime + end +end diff --git a/db/migrate/20161223034433_add_time_estimate_to_issuables.rb b/db/migrate/20161223034433_add_time_estimate_to_issuables.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d89756a9bcd6a3b7ca5b21f215faf1ae22f9b1d --- /dev/null +++ b/db/migrate/20161223034433_add_time_estimate_to_issuables.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTimeEstimateToIssuables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :issues, :time_estimate, :integer + add_column :merge_requests, :time_estimate, :integer + end +end diff --git a/db/migrate/20161223034646_create_timelogs.rb b/db/migrate/20161223034646_create_timelogs.rb new file mode 100644 index 0000000000000000000000000000000000000000..d3353a67eec086123bb2b991dc5ee75d77e7b08f --- /dev/null +++ b/db/migrate/20161223034646_create_timelogs.rb @@ -0,0 +1,38 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :timelogs do |t| + t.integer :time_spent, null: false + t.references :trackable, polymorphic: true + t.references :user + + t.timestamps null: false + end + + add_index :timelogs, [:trackable_type, :trackable_id] + add_index :timelogs, :user_id + end +end diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb new file mode 100644 index 0000000000000000000000000000000000000000..a0ce927161f1e192c2d67e51b8030fa3b597439f --- /dev/null +++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb @@ -0,0 +1,114 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDotGitFromUsernames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + invalid_users.each do |user| + id = user['id'] + namespace_id = user['namespace_id'] + path_was = user['username'] + path_was_wildcard = quote_string("#{path_was}/%") + path = quote_string(new_path(path_was)) + + move_namespace(namespace_id, path_was, path) + + begin + execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}" + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}" + execute "UPDATE users SET username = '#{path}' WHERE id = #{id}" + + select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route| + new_path = "#{path}/#{route['path'].split('/').last}" + execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}" + end + rescue => e + say("Couldn't update routes for path #{path_was} to #{path}") + # Move namespace back + move_namespace(namespace_id, path, path_was) + + raise e + end + end + end + + def down + # nothing to do here + end + + private + + def invalid_users + select_all("SELECT u.id, u.username, n.path AS namespace_path, n.id AS namespace_id FROM users u + INNER JOIN namespaces n ON n.owner_id = u.id + WHERE n.type is NULL AND n.path LIKE '%.git'") + end + + def route_exists?(path) + select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? + end + + def path_exists?(path, repository_storage_path) + repository_storage_path && gitlab_shell.exists?(repository_storage_path, path) + end + + # Accepts invalid path like test.git and returns test_git or + # test_git1 if test_git already taken + def new_path(path) + # To stay closer with original name and reduce risk of duplicates + # we rename suffix instead of removing it + path = path.sub(/\.git\z/, '_git') + + check_routes(path.dup, 0, path) + end + + def check_routes(base, counter, path) + route_exists = route_exists?(path) + + Gitlab.config.repositories.storages.each_value do |storage| + if route_exists || path_exists?(path, storage) + counter += 1 + path = "#{base}#{counter}" + + return check_routes(base, counter, path) + end + end + + path + end + + def move_namespace(namespace_id, path_was, path) + repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row| + Gitlab.config.repositories.storages[row['repository_storage']] + end.compact + + # Move the namespace directory in all storages paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + + begin + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + rescue => e + if path.nil? + say("Couldn't find a storage path for #{namespace_id}, #{path_was} -- skipping") + else + raise e + end + end + end +end diff --git a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb new file mode 100644 index 0000000000000000000000000000000000000000..50ad74372274219f2f4f9861c0266d3eff343e46 --- /dev/null +++ b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb @@ -0,0 +1,25 @@ +class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + update_column_in_batches(:services, :type, 'SlackService') do |table, query| + query.where(table[:type].eq('SlackNotificationService')) + end + + update_column_in_batches(:services, :type, 'MattermostService') do |table, query| + query.where(table[:type].eq('MattermostNotificationService')) + end + end + + def down + update_column_in_batches(:services, :type, 'SlackNotificationService') do |table, query| + query.where(table[:type].eq('SlackService')) + end + + update_column_in_batches(:services, :type, 'MattermostNotificationService') do |table, query| + query.where(table[:type].eq('MattermostService')) + end + end +end diff --git a/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb new file mode 100644 index 0000000000000000000000000000000000000000..7df561d82dd4ce7196f3837ef68e1eff7d1584a8 --- /dev/null +++ b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb @@ -0,0 +1,19 @@ +# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`. +# It's easier to achieve this by adding the column with the `['api']` default (regular migration), and +# then changing the default to `[]` (in this post-migration). +# +# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951#note_19721973 + +class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_default :personal_access_tokens, :scopes, [].to_yaml + end + + def down + change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml + end +end diff --git a/db/post_migrate/20161221140236_remove_unneeded_services.rb b/db/post_migrate/20161221140236_remove_unneeded_services.rb new file mode 100644 index 0000000000000000000000000000000000000000..6b7e94c864124cefc1dfbbba4b2a85d10d0dd93d --- /dev/null +++ b/db/post_migrate/20161221140236_remove_unneeded_services.rb @@ -0,0 +1,15 @@ +class RemoveUnneededServices < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + disable_statement_timeout + + execute("DELETE FROM services WHERE active = false AND properties = '{}';") + end + + def down + # noop + end +end diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb new file mode 100644 index 0000000000000000000000000000000000000000..282837be1fa3d7bc74934500e4edb55e70fc36ae --- /dev/null +++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb @@ -0,0 +1,130 @@ +require 'thread' + +class RenameReservedProjectNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + DOWNTIME = false + + THREAD_COUNT = 8 + + KNOWN_PATHS = %w(.well-known + all + blame + blob + commits + create + create_dir + edit + files + find_file + groups + hooks + issues + logs_tree + merge_requests + new + preview + projects + raw + repository + robots.txt + s + snippets + teams + tree + u + unsubscribes + update + users + wikis) + + def up + queues = Array.new(THREAD_COUNT) { Queue.new } + start = false + + threads = Array.new(THREAD_COUNT) do |index| + Thread.new do + queue = queues[index] + + # Wait until we have input to process. + until start; end + + rename_projects(queue.pop) until queue.empty? + end + end + + enum = queues.each + + reserved_projects.each_slice(100) do |slice| + begin + queue = enum.next + rescue StopIteration + enum.rewind + retry + end + + queue << slice + end + + start = true + + threads.each(&:join) + end + + def down + # nothing to do here + end + + private + + def reserved_projects + Project.unscoped. + includes(:namespace). + where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)'). + where('projects.path' => KNOWN_PATHS) + end + + def route_exists?(full_path) + quoted_path = ActiveRecord::Base.connection.quote_string(full_path) + + ActiveRecord::Base.connection. + select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present? + end + + # Adds number to the end of the path that is not taken by other route + def rename_path(namespace_path, path_was) + counter = 0 + path = "#{path_was}#{counter}" + + while route_exists?("#{namespace_path}/#{path}") + counter += 1 + path = "#{path_was}#{counter}" + end + + path + end + + def rename_projects(projects) + projects.each do |project| + id = project.id + path_was = project.path + namespace_path = project.namespace.path + path = rename_path(namespace_path, path_was) + + begin + # Because project path update is quite complex operation we can't safely + # copy-paste all code from GitLab. As exception we use Rails code here + project.rename_repo if rename_project_row(project, path) + rescue Exception => e # rubocop: disable Lint/RescueException + Rails.logger.error "Exception when renaming project #{id}: #{e.message}" + end + end + end + + def rename_project_row(project, path) + project.respond_to?(:update_attributes) && + project.update_attributes(path: path) && + project.respond_to?(:rename_repo) + end +end diff --git a/db/post_migrate/20170106142508_fill_authorized_projects.rb b/db/post_migrate/20170106142508_fill_authorized_projects.rb new file mode 100644 index 0000000000000000000000000000000000000000..314c8440c8b229135add22caef73f989874cabf2 --- /dev/null +++ b/db/post_migrate/20170106142508_fill_authorized_projects.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FillAuthorizedProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + class User < ActiveRecord::Base + self.table_name = 'users' + end + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # We're not inserting any data so we don't need to start a transaction. + disable_ddl_transaction! + + def up + relation = User.select(:id). + where('authorized_projects_populated IS NOT TRUE') + + relation.find_in_batches(batch_size: 1_000) do |rows| + args = rows.map { |row| [row.id] } + + Sidekiq::Client.push_bulk('class' => 'AuthorizedProjectsWorker', 'args' => args) + end + end + + def down + end +end diff --git a/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb b/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c788160022907bc647d1bedadcab22454fdecb5 --- /dev/null +++ b/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveProjectAuthorizationsIdColumn < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + remove_column :project_authorizations, :id, :primary_key + end +end diff --git a/db/schema.rb b/db/schema.rb index ae47456084ffe42f17788b4e58f1ddb7641688e6..7815392c1c3b84fb785ac5b182ba66e2ede806a8 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: 20161212142807) do +ActiveRecord::Schema.define(version: 20170106172224) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,15 +98,17 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true + t.string "plantuml_url" + t.boolean "plantuml_enabled" end create_table "audit_events", force: :cascade do |t| @@ -428,9 +430,11 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.string "external_url" t.string "environment_type" t.string "state", default: "available", null: false + t.string "slug", null: false end - add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree + add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree + add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree create_table "events", force: :cascade do |t| t.string "target_type" @@ -502,6 +506,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.integer "lock_version" t.text "title_html" t.text "description_html" + t.integer "time_estimate" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -525,6 +530,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.string "type" t.string "fingerprint" t.boolean "public", default: false, null: false + t.boolean "can_push", default: false, null: false + t.datetime "last_used_at" end add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree @@ -679,6 +686,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.integer "lock_version" t.text "title_html" t.text "description_html" + t.integer "time_estimate" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -737,18 +745,18 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.text "description_html" t.boolean "lfs_enabled" + t.text "description_html" t.integer "parent_id" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree + add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree @@ -852,12 +860,13 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.datetime "expires_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "scopes", default: "--- []\n", null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree - create_table "project_authorizations", force: :cascade do |t| + create_table "project_authorizations", id: false, force: :cascade do |t| t.integer "user_id" t.integer "project_id" t.integer "access_level" @@ -898,6 +907,19 @@ ActiveRecord::Schema.define(version: 20161212142807) do add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree + create_table "project_statistics", force: :cascade do |t| + t.integer "project_id", null: false + t.integer "namespace_id", null: false + t.integer "commit_count", limit: 8, default: 0, null: false + t.integer "storage_size", limit: 8, default: 0, null: false + t.integer "repository_size", limit: 8, default: 0, null: false + t.integer "lfs_objects_size", limit: 8, default: 0, null: false + t.integer "build_artifacts_size", limit: 8, default: 0, null: false + end + + add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree + add_index "project_statistics", ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree + create_table "projects", force: :cascade do |t| t.string "name" t.string "path" @@ -912,11 +934,9 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" t.boolean "shared_runners_enabled", default: true, null: false @@ -1110,6 +1130,18 @@ ActiveRecord::Schema.define(version: 20161212142807) do add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree + create_table "timelogs", force: :cascade do |t| + t.integer "time_spent", null: false + t.integer "trackable_id" + t.string "trackable_type" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree + add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree + create_table "todos", force: :cascade do |t| t.integer "user_id", null: false t.integer "project_id", null: false @@ -1219,8 +1251,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "incoming_email_token" t.string "organization" + t.string "incoming_email_token" t.boolean "authorized_projects_populated" end @@ -1285,6 +1317,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do add_foreign_key "personal_access_tokens", "users" add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade + add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "subscriptions", "projects", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index eba1e9845b10fe3fd986ddabd49c45a56d9aad3f..ee69684b53b1a05edc1415b5b0c945919f80c6b6 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,7 +8,7 @@ - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry. - [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. -- [Importing to GitLab](workflow/importing/README.md). +- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab. - [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Markdown](user/markdown.md) GitLab's advanced formatting system. - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. @@ -34,6 +34,7 @@ - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. +- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab. - [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 07e548aaabe7a244a8abfbfe83a07ed4348c8006..13bd501e3977849b40115c85e2a4104cc7734a75 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -6,6 +6,7 @@ providers. - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, and 389 Server - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - Bitbucket, Facebook, Shibboleth, Crowd and Azure -- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider + Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS +- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [Okta](okta.md) Configure GitLab to sign in using Okta diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md new file mode 100644 index 0000000000000000000000000000000000000000..3f39539da95eb7baafc41b2ff49c9b8228a5f532 --- /dev/null +++ b/doc/administration/auth/authentiq.md @@ -0,0 +1,69 @@ +# Authentiq OmniAuth Provider + +To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq. + +Authentiq will generate a Client ID and the accompanying Client Secret for you to use. + +1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register). + +2. On your GitLab server, open the configuration file: + + For omnibus installation + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml + ``` + +3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider. + +4. Add the provider configuration for Authentiq: + + For Omnibus packages: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "authentiq", + "app_id" => "YOUR_CLIENT_ID", + "app_secret" => "YOUR_CLIENT_SECRET", + "args" => { + scope: 'aq:name email~rs aq:push' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'authentiq', + app_id: 'YOUR_CLIENT_ID', + app_secret: 'YOUR_CLIENT_SECRET', + args: { + scope: 'aq:name email~rs aq:push' + } + } + ``` + + +5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. +See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers. + +6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1. + +7. Save the configuration file. + +8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source) + for the changes to take effect if you installed GitLab via Omnibus or from source respectively. + +On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process. + +- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation. +- If not they will be prompted to download the app and then follow the procedure above. + +If everything goes right, the user will be returned to GitLab and will be signed in. \ No newline at end of file diff --git a/doc/administration/auth/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..12e219567158d918c4aac56b6c28e80360418de4 Binary files /dev/null and b/doc/administration/auth/img/okta_admin_panel.png differ diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..ee275ece369f9cc5ddf4d9de2ff78aba8d14e016 Binary files /dev/null and b/doc/administration/auth/img/okta_saml_settings.png differ diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index b8b63df091e677c4ef659a8229b2f44f2de4fd35..047233652770f7dc2d43ff3492a206c64f210247 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -298,8 +298,11 @@ LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. -### Login with valid credentials rejected +### Troubleshooting -If there is an unexpected error while authenticating the user with the LDAP -backend, the login is rejected and details about the error are logged to +If a user account is blocked or unblocked due to the LDAP configuration, a +message will be logged to `application.log`. + +If there is an unexpected error during an LDAP lookup (configuration error, +timeout), the login is rejected and a message will be logged to `production.log`. diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md new file mode 100644 index 0000000000000000000000000000000000000000..cb42b7743c5f782d16d43c66e4c8c0c449f33bfb --- /dev/null +++ b/doc/administration/auth/okta.md @@ -0,0 +1,160 @@ +# Okta SSO provider + +Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate +with GitLab. + +The following documentation enables Okta as a SAML provider. + +## Configure the Okta application + +1. On Okta go to the admin section and choose to **Add an App**. +1. When the app screen comes up you see another button to **Create an App** and + choose SAML 2.0 on the next screen. +1. Now, very important, add a logo + (you can choose it from https://about.gitlab.com/press/). You'll have to + crop and resize it. +1. Next, you'll need the to fill in the SAML general config. Here's an example + image. + +  + +1. The last part of the configuration is the feedback section where you can + just say you're a customer and creating an app for internal use. +1. When you have your app you'll have a few tabs on the top of the app's + profile. Click on the SAML 2.0 config instructions button which should + look like the following: + +  + +1. On the screen that comes up take note of the + **Identity Provider Single Sign-On URL** which you'll use for the + `idp_sso_target_url` on your GitLab config file. + +1. **Before you leave Okta make sure you add your user and groups if any.** + +--- + +Now that the Okta app is configured, it's time to enable it in GitLab. + +## Configure GitLab + +1. On your GitLab server, open the configuration file: + + **For Omnibus GitLab installations** + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + **For installations from source** + + ```sh + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) + for initial settings. + +1. To allow your users to use Okta to sign up without having to manually create + an account first, don't forget to add the following values to your + configuration: + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_allow_single_sign_on'] = ['saml'] + gitlab_rails['omniauth_block_auto_created_users'] = false + ``` + + **For installations from source** + + ```yaml + allow_single_sign_on: ["saml"] + block_auto_created_users: false + ``` + +1. You can also automatically link Okta users with existing GitLab users if + their email addresses match by adding the following setting: + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_auto_link_saml_user'] = true + ``` + + **For installations from source** + + ```yaml + auto_link_saml_user: true + ``` + +1. Add the provider configuration. + + >**Notes:** + >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint + of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab + installation to generate the correct value). + >- To get the `idp_cert_fingerprint` fingerprint, first download the + certificate from the Okta app you registered and then run: + `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert` + with the location of your certificate. + >- Change the value of `idp_sso_target_url`, with the value of the + **Identity Provider Single Sign-On URL** from the step when you + configured the Okta app. + >- Change the value of `issuer` to a unique name, which will identify the application + to the IdP. + >- Leave `name_identifier_format` as-is. + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }, + label: 'Okta' # optional label for SAML login button, defaults to "Saml" + } + ] + ``` + + **For installations from source** + + ```yaml + - { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }, + label: 'Okta' # optional label for SAML login button, defaults to "Saml" + } + ``` + + +1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations + from source respectively for the changes to take effect. + +You might want to try this out on a incognito browser window. + +## Configuring groups + +>**Note:** +Make sure the groups exist and are assigned to the Okta app. + +You can take a look of the [SAML documentation][saml] on external groups since +it works the same. + +[okta-sso]: https://www.okta.com/products/single-sign-on/ +[saml]: ../../integration/saml.md#external-groups +[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../restart_gitlab.md#installations-from-source diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md index 3ba8387c7f0e7c14fa87dc16cb0953f1147c928d..cca422892ec62840fedd1e1dd1f80670950a53e2 100644 --- a/doc/administration/build_artifacts.md +++ b/doc/administration/build_artifacts.md @@ -88,3 +88,9 @@ artifacts through the [Admin area settings](../user/admin_area/settings/continuo [reconfigure gitlab]: restart_gitlab.md "How to restart GitLab" [restart gitlab]: restart_gitlab.md "How to restart GitLab" + +## Storage statistics + +You can see the total storage used for build artifacts on groups and projects +in the administration area, as well as through the [groups](../api/groups.md) +and [projects APIs](../api/projects.md). diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 06291705702b8fc8c755758a667c4b791f3b8aa3..80e5d80aa41fa8757c0f660b724cebaf3ae5a790 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -44,22 +44,30 @@ as appropriate. ## Chained hooks support -> [Introduced][93] in GitLab Shell 4.1.0. +> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15. -The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project) -directories supporting chained execution of the hooks. +Hooks can be also placed in `hooks/<hook_name>.d` (global) or +`custom_hooks/<hook_name>.d` (per project) directories supporting chained +execution of the hooks. + +To look in a different directory for the global custom hooks (those in +`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For +Omnibus installations, this can be set in `gitlab.rb`; and in source +installations, this can be set in `gitlab-shell/config.yml`. The hooks are searched and executed in this order: + 1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir 1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>` 1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior) 1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks -1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files) +1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files) -Files in `.d` directories need to be executable and not match the backup file pattern (`*~`). +Files in `.d` directories need to be executable and not match the backup file +pattern (`*~`). -The hooks of the same type are executed in order and execution stops on the first -script exiting with non-zero value. +The hooks of the same type are executed in order and execution stops on the +first script exiting with a non-zero value. ## Custom error messages diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index b4a953d1ccc0a0aea42cf07335b463f9b4f6c1d3..76029b30dd87be973182508c1a46e5145b2346e8 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,17 +13,18 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- -`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation -`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) -`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` -`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` -`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab -`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer -`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation +`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) +`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` +`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` +`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab +`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for GitLab Runners ## Complete database variables diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index b36cf18d459325fb3dcc091fce45c6e86b861684..e4f94eb7cb68d44fbbd04216f94f72ae27f8cee8 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -44,6 +44,9 @@ If you use a cloud-managed service, or provide your own PostgreSQL: gitlab_rails['db_password'] = 'DB password' postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0'] postgresql['listen_address'] = '0.0.0.0' + + # Disable automatic database migrations + gitlab_rails['auto_migrate'] = false ``` 1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. @@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Exit the database prompt by typing `\q` and Enter. 1. Exit the `gitlab-psql` user by running `exit` twice. 1. Run `sudo gitlab-ctl reconfigure` a final time. -1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations - from running on upgrade. Only the primary GitLab application server should - handle migrations. --- diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 136f570ac2724388853e47ea0694042d59d5e7a0..1824829903cba99863dfdc33d28e35f71fcbc497 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -10,11 +10,11 @@ you need to use with GitLab. ## Basic ports -| LB Port | Backend Port | Protocol | -| ------- | ------------ | -------- | -| 80 | 80 | HTTP | -| 443 | 443 | HTTPS [^1] | -| 22 | 22 | TCP | +| LB Port | Backend Port | Protocol | +| ------- | ------------ | --------------- | +| 80 | 80 | HTTP [^1] | +| 443 | 443 | HTTPS [^1] [^2] | +| 22 | 22 | TCP | ## GitLab Pages Ports @@ -25,8 +25,8 @@ GitLab Pages requires a separate VIP. Configure DNS to point the | LB Port | Backend Port | Protocol | | ------- | ------------ | -------- | -| 80 | Varies [^2] | HTTP | -| 443 | Varies [^2] | TCP [^3] | +| 80 | Varies [^3] | HTTP | +| 443 | Varies [^3] | TCP [^4] | ## Alternate SSH Port @@ -50,13 +50,19 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) -[^1]: When using HTTPS protocol for port 443, you will need to add an SSL +[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires + your load balancer to correctly handle WebSocket connections. When using + HTTP or HTTPS proxying, this means your load balancer must be configured + to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the + [web terminal](../integration/terminal.md) integration guide for + more details. +[^2]: When using HTTPS protocol for port 443, you will need to add an SSL certificate to the load balancers. If you wish to terminate SSL at the GitLab application server instead, use TCP protocol. -[^2]: The backend port for GitLab Pages depends on the +[^3]: The backend port for GitLab Pages depends on the `gitlab_pages['external_http']` and `gitlab_pages['external_https']` setting. See [GitLab Pages documentation][gitlab-pages] for more details. -[^3]: Port 443 for GitLab Pages should always use the TCP protocol. Users can +[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index f532a106bc6462de19ab32a66c4e66622c72c922..b4e7bf21e359143592bc1f1cae65fb08b5be11f7 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -287,14 +287,14 @@ The prerequisites for a HA Redis setup are the following: redis['password'] = 'redis-password-goes-here' ``` -1. To prevent database migrations from running on upgrade, run: +1. Only the primary GitLab application server should handle migrations. To + prevent database migrations from running on upgrade, add the following + configuration to your `/etc/gitlab/gitlab.rb` file: ``` - sudo touch /etc/gitlab/skip-auto-migrations + gitlab_rails['auto_migrate'] = false ``` - Only the primary GitLab application server should handle migrations. - 1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect. ### Step 2. Configuring the slave Redis instances diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index f846c06ca4224fa39a32c5b77563fe0b7c559ee4..bb621b788f1e5d062fbc2cd6d166c89c2d2ca8f9 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -12,13 +12,24 @@ to turn it off, go to **Admin area > Settings** ## Manual housekeeping -The housekeeping function runs `git gc` ([man page][man]) on the current -project Git repository. +The housekeeping function will run a `repack` or `gc` depending on the +"Automatic Git repository housekeeping" settings configured in **Admin area > Settings** -`git gc` runs a number of housekeeping tasks, such as compressing file -revisions (to reduce disk space and increase performance) and removing -unreachable objects which may have been created from prior invocations of -`git add`. +For example in the following scenario a `git repack -d` will be executed: + ++ Project: pushes since gc counter (`pushes_since_gc`) = `10` ++ Git GC period = `200` ++ Full repack period = `50` + +When the `pushes_since_gc` value is 50 a `repack -A -d --pack-kept-objects` will run, similarly when +the `pushes_since_gc` value is 200 a `git gc` will be run. + ++ `git gc` ([man page][man-gc]) runs a number of housekeeping tasks, +such as compressing filerevisions (to reduce disk space and increase performance) +and removing unreachable objects which may have been created from prior invocations of +`git add`. + ++ `git repack` ([man page][man-repack]) re-organize existing packs into a single, more efficient pack. You can find this option under your **[Project] > Edit Project**. @@ -27,4 +38,5 @@ You can find this option under your **[Project] > Edit Project**.  [ce-2371]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2371 "Housekeeping merge request" -[man]: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html "git gc man page" +[man-gc]: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html "git gc man page" +[man-repack]: https://www.kernel.org/pub/software/scm/git/docs/git-repack.html \ No newline at end of file diff --git a/doc/administration/img/integration/plantuml-example.png b/doc/administration/img/integration/plantuml-example.png new file mode 100644 index 0000000000000000000000000000000000000000..cb64eca1a8ae7111d3cfe18165ec91015f6566d7 Binary files /dev/null and b/doc/administration/img/integration/plantuml-example.png differ diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md new file mode 100644 index 0000000000000000000000000000000000000000..e5cf592e0a660757847379006028235dbec58223 --- /dev/null +++ b/doc/administration/integration/plantuml.md @@ -0,0 +1,87 @@ +# PlantUML & GitLab + +> [Introduced][ce-7810] in GitLab 8.16. + +When [PlantUML](http://plantuml.com) integration is enabled and configured in +GitLab we are able to create simple diagrams in AsciiDoc documents created in +snippets, wikis, and repos. + +## PlantUML Server + +Before you can enable PlantUML in GitLab; you need to set up your own PlantUML +server that will generate the diagrams. Installing and configuring your +own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat. + +First you need to create a `plantuml.war` file from the source code: + +``` +sudo apt-get install graphviz openjdk-7-jdk git-core maven +git clone https://github.com/plantuml/plantuml-server.git +cd plantuml-server +mvn package +``` + +The above sequence of commands will generate a WAR file that can be deployed +using Tomcat: + +``` +sudo apt-get install tomcat7 +sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war +sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war +sudo service restart tomcat7 +``` + +Once the Tomcat service restarts the PlantUML service will be ready and +listening for requests on port 8080: + +``` +http://localhost:8080/plantuml +``` + +you can change these defaults by editing the `/etc/tomcat7/server.xml` file. + + +## GitLab + +You need to enable PlantUML integration from Settings under Admin Area. To do +that, login with an Admin account and do following: + + - in GitLab go to **Admin Area** and then **Settings** + - scroll to bottom of the page until PlantUML section + - check **Enable PlantUML** checkbox + - set the PlantUML instance as **PlantUML URL** + +## Creating Diagrams + +With PlantUML integration enabled and configured, we can start adding diagrams to +our AsciiDoc snippets, wikis and repos using blocks: + +``` +[plantuml, format="png", id="myDiagram", width="200px"] +-- +Bob->Alice : hello +Alice -> Bob : Go Away +-- +``` + +The above block will be converted to an HTML img tag with source pointing to the +PlantUML instance. If the PlantUML server is correctly configured, this should +render a nice diagram instead of the block: + + + +Inside the block you can add any of the supported diagrams by PlantUML such as +[Sequence](http://plantuml.com/sequence-diagram), [Use Case](http://plantuml.com/use-case-diagram), +[Class](http://plantuml.com/class-diagram), [Activity](http://plantuml.com/activity-diagram-legacy), +[Component](http://plantuml.com/component-diagram), [State](http://plantuml.com/state-diagram), +and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML +diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block. + +Some parameters can be added to the block definition: + + - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by + all browsers so use with care. The default is `png`. + - *id*: A CSS id added to the diagram HTML tag. + - *width*: Width attribute added to the img tag. + - *height*: Height attribute added to the img tag. + diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md new file mode 100644 index 0000000000000000000000000000000000000000..a1d1bb03b50df90c27633d8a0414e61f63310d50 --- /dev/null +++ b/doc/administration/integration/terminal.md @@ -0,0 +1,73 @@ +# Web terminals + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) +in GitLab 8.15. Only project masters and owners can access web terminals. + +With the introduction of the [Kubernetes](../../project_services/kubernetes.md) +project service, GitLab gained the ability to store and use credentials for a +Kubernetes cluster. One of the things it uses these credentials for is providing +access to [web terminals](../../ci/environments.html#web-terminals) +for environments. + +## How it works + +A detailed overview of the architecture of web terminals and how they work +can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md). +In brief: + +* GitLab relies on the user to provide their own Kubernetes credentials, and to + appropriately label the pods they create when deploying. +* When a user navigates to the terminal page for an environment, they are served + a JavaScript application that opens a WebSocket connection back to GitLab. +* The WebSocket is handled in [Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse), + rather than the Rails application server. +* Workhorse queries Rails for connection details and user permissions; Rails + queries Kubernetes for them in the background, using [Sidekiq](../troubleshooting/sidekiq.md) +* Workhorse acts as a proxy server between the user's browser and the Kubernetes + API, passing WebSocket frames between the two. +* Workhorse regularly polls Rails, terminating the WebSocket connection if the + user no longer has permission to access the terminal, or if the connection + details have changed. + +## Enabling and disabling terminal support + +As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of +Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers +through to the next one in the chain. If you installed Gitlab using Omnibus, or +from source, starting with GitLab 8.15, this should be done by the default +configuration, so there's no need for you to do anything. + +However, if you run a [load balancer](../high_availability/load_balancer.md) in +front of GitLab, you may need to make some changes to your configuration. These +guides document the necessary steps for a selection of popular reverse proxies: + +* [Apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) +* [NGINX](https://www.nginx.com/blog/websocket-nginx/) +* [HAProxy](http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/) +* [Varnish](https://www.varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html) + +Workhorse won't let WebSocket requests through to non-WebSocket endpoints, so +it's safe to enable support for these headers globally. If you'd rather had a +narrower set of rules, you can restrict it to URLs ending with `/terminal.ws` +(although this may still have a few false positives). + +If you installed from source, or have made any configuration changes to your +Omnibus installation before upgrading to 8.15, you may need to make some +changes to your configuration. See the [8.14 to 8.15 upgrade](../../update/8.14-to-8.15.md#nginx-configuration) +document for more details. + +If you'd like to disable web terminal support in GitLab, just stop passing +the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse +proxy in the chain. For most users, this will be the NGINX server bundled with +Omnibus Gitlab, in which case, you need to: + +* Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file +* Ensure the whole block is uncommented, and then comment out or remove the + `Connection` and `Upgrade` lines. + +For your own load balancer, just reverse the configuration changes recommended +by the above guides. + +When these headers are not passed through, Workhorse will return a +`400 Bad Request` response to users attempting to use a web terminal. In turn, +they will receive a `Connection failed` message. diff --git a/doc/administration/logs.md b/doc/administration/logs.md index d757a3c2a6661c6bd9b91e6a085eb541fe75f5c8..4b8d5c5cc87f73be4a966743a7ad4054d1f8035b 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -1,4 +1,4 @@ -## Log system +# Log system GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files. In addition to @@ -9,10 +9,10 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. -### production.log +## `production.log` This file lives in `/var/log/gitlab/gitlab-rails/production.log` for -omnibus package or in `/home/git/gitlab/log/production.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/production.log` for installations from source. (When Gitlab is running in an environment other than production, the corresponding logfile is shown here.) @@ -46,10 +46,10 @@ In this example we can see that server processed an HTTP request with URL 19:34:53 +0200. Also we can see that request was processed by `Projects::TreeController`. -### application.log +## `application.log` This file lives in `/var/log/gitlab/gitlab-rails/application.log` for -omnibus package or in `/home/git/gitlab/log/application.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/application.log` for installations from source. It helps you discover events happening in your instance such as user creation, @@ -63,10 +63,10 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was October 07, 2014 11:25: Project "project133" was removed ``` -### githost.log +## `githost.log` This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for -omnibus package or in `/home/git/gitlab/log/githost.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/githost.log` for installations from source. GitLab has to interact with Git repositories but in some rare cases @@ -81,10 +81,10 @@ December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/ error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git' ``` -### sidekiq.log +## `sidekiq.log` This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for -omnibus package or in `/home/git/gitlab/log/sidekiq.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for installations from source. GitLab uses background jobs for processing tasks which can take a long @@ -96,10 +96,10 @@ this file. For example: 2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"} ``` -### gitlab-shell.log +## `gitlab-shell.log` This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for -omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for +Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from source. GitLab shell is used by Gitlab for executing Git commands and provide @@ -110,10 +110,10 @@ I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. ``` -### unicorn\_stderr.log +## `unicorn\_stderr.log` This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for -omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from source. Unicorn is a high-performance forking Web server which is used for @@ -136,3 +136,13 @@ I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9 I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379 I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready ``` + +## `repocheck.log` + +This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for +installations from source. + +It logs information whenever a [repository check is run][repocheck] on a project. + +[repocheck]: repository_checks.md diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index d1d2fed486126c43ddf39430a0af7977d78637f0..c8b5434c068340cb7941e5bc03cb9d4336ebae3d 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -74,24 +74,5 @@ Example output: The LDAP check Rake task will test the bind_dn and password credentials (if configured) and will list a sample of LDAP users. This task is also -executed as part of the `gitlab:check` task, but can run independently -using the command below. - -**Omnibus Installation** - -``` -sudo gitlab-rake gitlab:ldap:check -``` - -**Source Installation** - -```bash -sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production -``` - -By default, the task will return a sample of 100 LDAP users. Change this -limit by passing a number to the check task: - -```bash -rake gitlab:ldap:check[50] -``` +executed as part of the `gitlab:check` task, but can run independently. +See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details. diff --git a/doc/administration/raketasks/ldap.md b/doc/administration/raketasks/ldap.md new file mode 100644 index 0000000000000000000000000000000000000000..91fc0133d56f450f16f6ccf5be31551799fb43ee --- /dev/null +++ b/doc/administration/raketasks/ldap.md @@ -0,0 +1,120 @@ +# LDAP Rake Tasks + +## Check + +The LDAP check Rake task will test the `bind_dn` and `password` credentials +(if configured) and will list a sample of LDAP users. This task is also +executed as part of the `gitlab:check` task, but can run independently +using the command below. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:ldap:check +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production +``` + +------ + +By default, the task will return a sample of 100 LDAP users. Change this +limit by passing a number to the check task: + +```bash +rake gitlab:ldap:check[50] +``` + +## Rename a provider + +If you change the LDAP server ID in `gitlab.yml` or `gitlab.rb` you will need +to update all user identities or users will be unable to sign in. Input the +old and new provider and this task will update all matching identities in the +database. + +`old_provider` and `new_provider` are derived from the prefix `ldap` plus the +LDAP server ID from the configuration file. For example, in `gitlab.yml` or +`gitlab.rb` you may see LDAP configuration like this: + +```yaml +main: + label: 'LDAP' + host: '_your_ldap_server' + port: 389 + uid: 'sAMAccountName' + ... +``` + +`main` is the LDAP server ID. Together, the unique provider is `ldapmain`. + +> **Warning**: If you input an incorrect new provider users will be unable +to sign in. If this happens, run the task again with the incorrect provider +as the `old_provider` and the correct provider as the `new_provider`. + +**Omnibus Installation** + +```bash +sudo gitlab-rake gitlab:ldap:rename_provider[old_provider,new_provider] +``` + +**Source Installation** + +```bash +bundle exec rake gitlab:ldap:rename_provider[old_provider,new_provider] RAILS_ENV=production +``` + +### Example + +Consider beginning with the default server ID `main` (full provider `ldapmain`). +If we change `main` to `mycompany`, the `new_provider` is `ldapmycompany`. +To rename all user identities run the following command: + +```bash +sudo gitlab-rake gitlab:ldap:rename_provider[ldapmain,ldapmycompany] +``` + +Example output: + +``` +100 users with provider 'ldapmain' will be updated to 'ldapmycompany'. +If the new provider is incorrect, users will be unable to sign in. +Do you want to continue (yes/no)? yes + +User identities were successfully updated +``` + +### Other options + +If you do not specify an `old_provider` and `new_provider` you will be prompted +for them: + +**Omnibus Installation** + +```bash +sudo gitlab-rake gitlab:ldap:rename_provider +``` + +**Source Installation** + +```bash +bundle exec rake gitlab:ldap:rename_provider RAILS_ENV=production +``` + +**Example output:** + +``` +What is the old provider? Ex. 'ldapmain': ldapmain +What is the new provider? Ex. 'ldapcustom': ldapmycompany +``` + +------ + +This tasks also accepts the `force` environment variable which will skip the +confirmation dialog: + +```bash +sudo gitlab-rake gitlab:ldap:rename_provider[old_provider,new_provider] force=yes +``` diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index 14cd7a03826b4d0527fb10dcb3e3c0cec38ebc35..00494e7e9d6823962fb034f7328666583bc7e4f1 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -71,8 +71,8 @@ If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255). -To set up a basic Postfix mail server with IMAP access on Ubuntu, follow -[these instructions](./postfix.md). +To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the +[Postfix setup documentation](reply_by_email_postfix_setup.md). ### Omnibus package installations diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 5f248ab6f91b5c2a8d2e13ec81e50b46224967d6..284d5f88c55e2b704dee9b3133236b556e9c0dca 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -20,12 +20,14 @@ Example response: "id": 1, "title": "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "can_push": false, "created_at": "2013-10-02T10:12:29Z" }, { "id": 3, "title": "Another Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "can_push": true, "created_at": "2013-10-02T11:12:29Z" } ] @@ -55,12 +57,14 @@ Example response: "id": 1, "title": "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "can_push": false, "created_at": "2013-10-02T10:12:29Z" }, { "id": 3, "title": "Another Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "can_push": false, "created_at": "2013-10-02T11:12:29Z" } ] @@ -92,6 +96,7 @@ Example response: "id": 1, "title": "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "can_push": false, "created_at": "2013-10-02T10:12:29Z" } ``` @@ -107,14 +112,15 @@ project only if original one was is accessible by the same user. POST /projects/:id/deploy_keys ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `title` | string | yes | New deploy key's title | -| `key` | string | yes | New deploy key | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `title` | string | yes | New deploy key's title | +| `key` | string | yes | New deploy key | +| `can_push` | boolean | no | Can deploy key push to the project's repository | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" ``` Example response: @@ -124,6 +130,7 @@ Example response: "key" : "ssh-rsa AAAA...", "id" : 12, "title" : "My deploy key", + "can_push": true, "created_at" : "2015-08-29T12:44:31.550Z" } ``` diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index 87a5fa67124996dcf9e80d93800ecb6d581b7353..1299aca8c45c7f499930034ec5578c106f188d7e 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -22,8 +22,9 @@ Example response: [ { "id": 1, - "name": "Env1", - "external_url": "https://env1.example.gitlab.com" + "name": "review/fix-foo", + "slug": "review-fix-foo-dfjre3", + "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com" } ] ``` @@ -54,6 +55,7 @@ Example response: { "id": 1, "name": "deploy", + "slug": "deploy", "external_url": "https://deploy.example.gitlab.com" } ``` @@ -85,6 +87,7 @@ Example response: { "id": 1, "name": "staging", + "slug": "staging", "external_url": "https://staging.example.gitlab.com" } ``` @@ -112,6 +115,7 @@ Example response: { "id": 1, "name": "deploy", + "slug": "deploy", "external_url": "https://deploy.example.gitlab.com" } ``` diff --git a/doc/api/groups.md b/doc/api/groups.md index 134d7bda22f19702441cbdd0d72fd391537b6444..f7807390e6862f0b67b0acf6eed2d66a82a25296 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -13,6 +13,7 @@ Parameters: | `search` | string | no | Return list of authorized groups matching the search criteria | | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | +| `statistics` | boolean | no | Include group statistics (admins only) | ``` GET /groups @@ -31,7 +32,6 @@ GET /groups You can search for groups by name or path, see below. -======= ## List owned groups Get a list of groups which are owned by the authenticated user. @@ -40,6 +40,12 @@ Get a list of groups which are owned by the authenticated user. GET /groups/owned ``` +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `statistics` | boolean | no | Include group statistics | + ## List a group's projects Get a list of projects in this group. @@ -329,7 +335,7 @@ POST /groups/:id/projects/:project_id Parameters: - `id` (required) - The ID or path of a group -- `project_id` (required) - The ID of a project +- `project_id` (required) - The ID or path of a project ## Update group diff --git a/doc/api/issues.md b/doc/api/issues.md index 119125bcd3dbb09ffb6dcc0c95e616b0a7addc25..b276d1ad9182570825bf41eb225ad135fda610ca 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -23,12 +23,15 @@ GET /issues?state=closed GET /issues?labels=foo GET /issues?labels=foo,bar GET /issues?labels=foo,bar&state=opened +GET /issues?milestone=1.0.0 +GET /issues?milestone=1.0.0&state=opened ``` | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | +| `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | @@ -709,6 +712,146 @@ Example response: } ``` +## Set a time estimate for an issue + +Sets an estimated time of work for this issue. + +``` +POST /projects/:id/issues/:issue_id/time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m +``` + +Example response: + +```json +{ + "human_time_estimate": "3h 30m", + "human_total_time_spent": null, + "time_estimate": 12600, + "total_time_spent": 0 +} +``` + +## Reset the time estimate for an issue + +Resets the estimated time for this issue to 0 seconds. + +``` +POST /projects/:id/issues/:issue_id/reset_time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Add spent time for an issue + +Adds spent time for this issue + +``` +POST /projects/:id/issues/:issue_id/add_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": "1h", + "time_estimate": 0, + "total_time_spent": 3600 +} +``` + +## Reset spent time for an issue + +Resets the total spent time for this issue to 0 seconds. + +``` +POST /projects/:id/issues/:issue_id/reset_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Get time tracking stats + +``` +GET /projects/:id/issues/:issue_id/time_stats +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats +``` + +Example response: + +```json +{ + "human_time_estimate": "2h", + "human_total_time_spent": "1h", + "time_estimate": 7200, + "total_time_spent": 3600 +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 662cc9da7335e06cc5516de164359d861e35ac27..7b005591545274dae11a1f2e7ea32b54c7f5c345 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1018,3 +1018,142 @@ Example response: }] } ``` +## Set a time estimate for a merge request + +Sets an estimated time of work for this merge request. + +``` +POST /projects/:id/merge_requests/:merge_request_id/time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge request | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m +``` + +Example response: + +```json +{ + "human_time_estimate": "3h 30m", + "human_total_time_spent": null, + "time_estimate": 12600, + "total_time_spent": 0 +} +``` + +## Reset the time estimate for a merge request + +Resets the estimated time for this merge request to 0 seconds. + +``` +POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge_request | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Add spent time for a merge request + +Adds spent time for this merge request + +``` +POST /projects/:id/merge_requests/:merge_request_id/add_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge request | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": "1h", + "time_estimate": 0, + "total_time_spent": 3600 +} +``` + +## Reset spent time for a merge request + +Resets the total spent time for this merge request to 0 seconds. + +``` +POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge_request | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Get time tracking stats + +``` +GET /projects/:id/merge_requests/:merge_request_id/time_stats +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge request | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats +``` + +Example response: + +```json +{ + "human_time_estimate": "2h", + "human_total_time_spent": "1h", + "time_estimate": 7200, + "total_time_spent": 3600 +} +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 0bc2a5210aa92c2b8cd359b58f565373502266dc..122075bbd115eabe4ec7776158b30073c87f7929 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -307,6 +307,8 @@ Parameters: | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of authorized projects matching the search criteria | +| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | +| `statistics` | boolean | no | Include project statistics | ### List starred projects @@ -325,6 +327,7 @@ Parameters: | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of authorized projects matching the search criteria | +| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | ### List ALL projects @@ -343,6 +346,7 @@ Parameters: | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of authorized projects matching the search criteria | +| `statistics` | boolean | no | Include project statistics | ### Get single project @@ -664,7 +668,6 @@ Parameters: | `path` | string | no | Custom repository name for new project. By default generated based on name | | `default_branch` | string | no | `master` by default | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | -| `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | diff --git a/doc/api/repositories.md b/doc/api/repositories.md index bcf8b955044520ac0bbb22ecba556e18a9ad739d..727617f1ecc53a9c891d5e46f41203f92c563fc1 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -2,7 +2,8 @@ ## List repository tree -Get a list of repository files and directories in a project. +Get a list of repository files and directories in a project. This endpoint can +be accessed without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/tree @@ -71,7 +72,8 @@ Parameters: ## Raw file content -Get the raw file contents for a file by commit SHA and path. +Get the raw file contents for a file by commit SHA and path. This endpoint can +be accessed without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/blobs/:sha @@ -85,7 +87,8 @@ Parameters: ## Raw blob content -Get the raw file contents for a blob by blob SHA. +Get the raw file contents for a blob by blob SHA. This endpoint can be accessed +without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/raw_blobs/:sha @@ -98,7 +101,8 @@ Parameters: ## Get file archive -Get an archive of the repository +Get an archive of the repository. This endpoint can be accessed without +authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/archive @@ -111,6 +115,9 @@ Parameters: ## Compare branches, tags or commits +This endpoint can be accessed without authentication if the repository is +publicly accessible. + ``` GET /projects/:id/repository/compare ``` @@ -163,7 +170,8 @@ Response: ## Contributors -Get repository contributors list +Get repository contributors list. This endpoint can be accessed without +authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/contributors diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index b8c9eb2c9a8d595a3cc1476f3a71df74c81a6021..8a6baed5987ee181f3d39feeb257c5d559ab4fb8 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -6,7 +6,9 @@ ## Get file from repository -Allows you to receive information about file in repository like name, size, content. Note that file content is Base64 encoded. +Allows you to receive information about file in repository like name, size, +content. Note that file content is Base64 encoded. This endpoint can be accessed +without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/files diff --git a/doc/api/services.md b/doc/api/services.md index 3dad953cd1e0e60ef6087ae50749f1605cc87d69..1466b8189b0386797073b88eb40e4e776fa04f41 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -703,9 +703,9 @@ Get Redmine service settings for a project. GET /projects/:id/services/redmine ``` -## Slack +## Slack notifications -A team communication tool for the 21st century +Receive event notifications in Slack ### Create/Edit Slack service @@ -737,6 +737,40 @@ Get Slack service settings for a project. GET /projects/:id/services/slack ``` +## Mattermost notifications + +Receive event notifications in Mattermost + +### Create/Edit Mattermost notifications service + +Set Mattermost service for a project. + +``` +PUT /projects/:id/services/mattermost +``` + +Parameters: + +- `webhook` (**required**) - https://mattermost.example/hooks/1298aff... +- `username` (optional) - username +- `channel` (optional) - #channel + +### Delete Mattermost notifications service + +Delete Mattermost Notifications service for a project. + +``` +DELETE /projects/:id/services/mattermost +``` + +### Get Mattermost notifications service settings + +Get Mattermost notifications service settings for a project. + +``` +GET /projects/:id/services/mattermost +``` + ## JetBrains TeamCity CI A continuous integration and build server diff --git a/doc/api/settings.md b/doc/api/settings.md index 218546aafea0e296e0bd2d980aa6ef49dffd60d5..f86c7cc2f941f016f385efd22f1cdafe4a7ebb59 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -44,7 +44,9 @@ Example response: "repository_storage": "default", "repository_storages": ["default"], "koding_enabled": false, - "koding_url": null + "koding_url": null, + "plantuml_enabled": false, + "plantuml_url": null } ``` @@ -79,6 +81,9 @@ PUT /application/settings | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. | | `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. | +| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources | +| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | +| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -111,6 +116,8 @@ Example response: "container_registry_token_expire_delay": 5, "repository_storage": "default", "koding_enabled": false, - "koding_url": null + "koding_url": null, + "plantuml_enabled": false, + "plantuml_url": null } ``` diff --git a/doc/ci/README.md b/doc/ci/README.md index 73bd2516d46933afba38b032c11e8ee900929103..dd14698e9cd5b51cef54c8edb78e90194e3104bf 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -23,6 +23,7 @@ - [CI/CD pipelines settings](../user/project/pipelines/settings.md) - [Review Apps](review_apps/index.md) - [Git submodules](git_submodules.md) Using Git submodules in your CI jobs +- [Auto deploy](autodeploy/index.md) ## Breaking changes diff --git a/doc/ci/autodeploy/img/auto_deploy_button.png b/doc/ci/autodeploy/img/auto_deploy_button.png new file mode 100644 index 0000000000000000000000000000000000000000..423e76a6cdaf3eb599210315357acd57f40fa898 Binary files /dev/null and b/doc/ci/autodeploy/img/auto_deploy_button.png differ diff --git a/doc/ci/autodeploy/img/auto_deploy_dropdown.png b/doc/ci/autodeploy/img/auto_deploy_dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..957870ec8c73484df9531a4fac3b950a3a934408 Binary files /dev/null and b/doc/ci/autodeploy/img/auto_deploy_dropdown.png differ diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md new file mode 100644 index 0000000000000000000000000000000000000000..630207ffa09a4f0b6df542343dcaab2e5a21053f --- /dev/null +++ b/doc/ci/autodeploy/index.md @@ -0,0 +1,40 @@ +# Auto deploy + +> [Introduced][mr-8135] in GitLab 8.15. + +Auto deploy is an easy way to configure GitLab CI for the deployment of your +application. GitLab Community maintains a list of `.gitlab-ci.yml` +templates for various infrastructure providers and deployment scripts +powering them. These scripts are responsible for packaging your application, +setting up the infrastructure and spinning up necessary services (for +example a database). + +You can use [project services][project-services] to store credentials to +your infrastructure provider and they will be available during the +deployment. + +## Supported templates + +The list of supported auto deploy templates is available [here][auto-deploy-templates]. + +## Configuration + +1. Enable a deployment [project service][project-services] to store your +credentials. For example, if you want to deploy to OpenShift you have to +enable [Kubernetes service][kubernetes-service]. +1. Configure GitLab Runner to use Docker or Kubernetes executor with +[privileged mode enabled][docker-in-docker]. +1. Navigate to the "Project" tab and click "Set up auto deploy" button. +  +1. Select a template. +  +1. Commit your changes and create a merge request. +1. Test your deployment configuration using a [Review App][review-app] that was +created automatically for you. + +[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 +[project-services]: ../../project_services/project_services.md +[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy +[kubernetes-service]: ../../project_services/kubernetes.md +[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../review_apps/index.md diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 9dd84a5ff81a257f95482eb6872c9776de499d00..98cd29c956786c46e06efc83da4a447cb161388a 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -25,7 +25,9 @@ Environments are like tags for your CI jobs, describing where code gets deployed Deployments are created when [jobs] deploy versions of code to environments, so every environment can have one or more deployments. GitLab keeps track of your deployments, so you always know what is currently being deployed on your -servers. +servers. If you have a deployment service such as [Kubernetes][kubernetes-service] +enabled for your project, you can use it to assist with your deployments, and +can even access a web terminal for your environment from within GitLab! To better understand how environments and deployments work, let's consider an example. We assume that you have already created a project in GitLab and set up @@ -86,6 +88,13 @@ will later see, is exposed in various places within GitLab. Each time a job that has an environment specified and succeeds, a deployment is recorded, remembering the Git SHA and environment name. +>**Note:** +Starting with GitLab 8.15, the environment name is exposed to the Runner in +two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is +the name given in `.gitlab-ci.yml` (with any variables expanded), while the +second is a "cleaned-up" version of the name, suitable for use in URLs, DNS, +etc. + To sum up, with the above `.gitlab-ci.yml` we have achieved that: - All branches will run the `test` and `build` jobs. @@ -157,7 +166,7 @@ that can be found in the deployments page job with the commit associated with it. >**Note:** -Bare in mind that your mileage will vary and it's entirely up to how you define +Bear in mind that your mileage will vary and it's entirely up to how you define the deployment process in the job's `script` whether the rollback succeeds or not. GitLab CI is just following orders. @@ -226,6 +235,46 @@ Remember that if your environment's name is `production` (all lowercase), then it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). Double the benefit! +## Web terminals + +>**Note:** +Web terminals were added in GitLab 8.15 and are only available to project +masters and owners. + +If you deploy to your environments with the help of a deployment service (e.g., +the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open +a terminal session to your environment! This is a very powerful feature that +allows you to debug issues without leaving the comfort of your web browser. To +enable it, just follow the instructions given in the service documentation. + +Once enabled, your environments will gain a "terminal" button: + + + +You can also access the terminal button from the page for a specific environment: + + + +Wherever you find it, clicking the button will take you to a separate page to +establish the terminal session: + + + +This works just like any other terminal - you'll be in the container created +by your deployment, so you can run shell commands and get responses in real +time, check the logs, try out configuration or code tweaks, etc. You can open +multiple terminals to the same environment - they each get their own shell +session - and even a multiplexer like `screen` or `tmux`! + +>**Note:** +Container-based deployments often lack basic tools (like an editor), and may +be stopped or restarted at any time. If this happens, you will lose all your +changes! Treat this as a debugging tool, not a comprehensive online IDE. You +can use [Koding](../administration/integration/koding.md) for online +development. + +--- + While this is fine for deploying to some stable environments like staging or production, what happens for branches? So far we haven't defined anything regarding deployments for branches other than `master`. Dynamic environments @@ -248,7 +297,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.review.example.com only: - branches except: @@ -259,15 +308,25 @@ Let's break it down in pieces. The job's name is `deploy_review` and it runs on the `deploy` stage. The `script` at this point is fictional, you'd have to use your own based on your deployment. Then, we set the `environment` with the `environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting -one. Since the [environment name][env-name] can contain also slashes (`/`), we -can use this pattern to distinguish between dynamic environments and the regular +one. Since the [environment name][env-name] can contain slashes (`/`), we can +use this pattern to distinguish between dynamic environments and the regular ones. So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME` -which takes the value of the branch name. We also use the same -`$CI_BUILD_REF_NAME` value in the `environment:url` so that the environment -can get a specific and distinct URL for each branch. Again, the way you set up -the webserver to serve these requests is based on your setup. +which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may +also contain `/`, or other characters that would be invalid in a domain name or +URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the +environment can get a specific and distinct URL for each branch. In this case, +given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something +like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up +the web server to serve these requests is based on your setup. + +You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.: +`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG` +here because it is guaranteed to be unique, but if you're using a workflow like +[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer +environment names to be more closely based on the branch name - the example +above would give you an URL like `https://100-do-the-thing.review.example.com` Last but not least, we tell the job to run [`only`][only] on branches [`except`][only] master. @@ -299,7 +358,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com only: - branches except: @@ -329,16 +388,16 @@ deploy_prod: A more realistic example would include copying files to a location where a webserver (NGINX) could then read and serve. The example below will copy the -`public` directory to `/srv/nginx/$CI_BUILD_REF_NAME/public`: +`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`: ```yaml review_app: stage: deploy script: - - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_NAME + - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_BUILD_REF_SLUG.example.com ``` It is assumed that the user has already setup NGINX and GitLab Runner in the @@ -346,7 +405,7 @@ server this job will run on. >**Note:** Be sure to check out the [limitations](#limitations) section for some edge -cases regarding naming of you branches and Review Apps. +cases regarding naming of your branches and Review Apps. --- @@ -418,7 +477,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com on_stop: stop_review only: - branches @@ -480,9 +539,8 @@ exist, you should see something like: ## Checkout deployments locally -Since 8.13, a reference in the git repository is saved for each deployment. So -knowing what the state is of your current environments is only a `git fetch` -away. +Since 8.13, a reference in the git repository is saved for each deployment, so +knowing the state of your current environments is only a `git fetch` away. In your git config, append the `[remote "<your-remote>"]` block with an extra fetch line: @@ -493,10 +551,6 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* ## Limitations -1. If the branch name contains special characters (`/`), and you use the - `$CI_BUILD_REF_NAME` variable to dynamically create environments, there might - be complications during your Review Apps deployment. Follow the - [issue 22849][ce-22849] for more information. 1. You are limited to use only the [CI predefined variables][variables] in the `environment: name`. If you try to re-use variables defined inside `script` as part of the environment name, it will not work. @@ -512,6 +566,7 @@ Below are some links you may find interesting: [Pipelines]: pipelines.md [jobs]: yaml/README.md#jobs [yaml]: yaml/README.md +[kubernetes-service]: ../project_services/kubernetes.md] [environments]: #environments [deployments]: #deployments [permissions]: ../user/permissions.md @@ -520,6 +575,6 @@ Below are some links you may find interesting: [only]: yaml/README.md#only-and-except [onstop]: yaml/README.md#environment-on_stop [ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015 -[ce-22849]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22849 +[gitlab-flow]: ../workflow/gitlab_flow.md [gitlab runner]: https://docs.gitlab.com/runner/ [git-strategy]: yaml/README.md#git-strategy diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index 08c10d391ea367d0dc553fdfd9ea7a3c375f42d9..42f15a27f1236fbe8f99bf48cb57c9f9e3d185d1 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -34,7 +34,7 @@ production: This project has three jobs: 1. `test` - used to test Rails application, 2. `staging` - used to automatically deploy staging environment every push to `master` branch -3. `production` - used to automatically deploy production environmnet for every created tag +3. `production` - used to automatically deploy production environment for every created tag ### Store API keys You'll need to create two variables in `Project > Variables`: diff --git a/doc/ci/img/environments_terminal_button_on_index.png b/doc/ci/img/environments_terminal_button_on_index.png new file mode 100644 index 0000000000000000000000000000000000000000..6f05b2aa343eedc7e20f6f3b2e3f4adb2aa6fbfb Binary files /dev/null and b/doc/ci/img/environments_terminal_button_on_index.png differ diff --git a/doc/ci/img/environments_terminal_button_on_show.png b/doc/ci/img/environments_terminal_button_on_show.png new file mode 100644 index 0000000000000000000000000000000000000000..9469fab99ab08195d0bb8cf91f702c71c5fc43a4 Binary files /dev/null and b/doc/ci/img/environments_terminal_button_on_show.png differ diff --git a/doc/ci/img/environments_terminal_page.png b/doc/ci/img/environments_terminal_page.png new file mode 100644 index 0000000000000000000000000000000000000000..fde1bf325a6546abb932bd7fa2a5923d16fec860 Binary files /dev/null and b/doc/ci/img/environments_terminal_page.png differ diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 03b9c4bb444a20bd4dd51b9189adbe39536044b7..f91b9d350f762e77e1655f20c90bc2c3fca89ee5 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -36,6 +36,37 @@ Clicking on a pipeline will show the builds that were run for that pipeline. Clicking on an individual build will show you its build trace, and allow you to cancel the build, retry it, or erase the build trace. +## How the pipeline duration is calculated + +Total running time for a given pipeline would exclude retries and pending +(queue) time. We could reduce this problem down to finding the union of +periods. + +So each job would be represented as a `Period`, which consists of +`Period#first` as when the job started and `Period#last` as when the +job was finished. A simple example here would be: + +* A (1, 3) +* B (2, 4) +* C (6, 7) + +Here A begins from 1, and ends to 3. B begins from 2, and ends to 4. +C begins from 6, and ends to 7. Visually it could be viewed as: + +``` +0 1 2 3 4 5 6 7 + AAAAAAA + BBBBBBB + CCCC +``` + +The union of A, B, and C would be (1, 4) and (6, 7), therefore the +total running time should be: + +``` +(4 - 1) + (7 - 6) => 4 +``` + ## Badges Build status and test coverage report badges are available. You can find their diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d142fe266a2b2ea78b099d50eae548c6545cc543..d3b9611b02ea4628a652c1705af343b712376848 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -13,6 +13,7 @@ this order: 1. [Secret variables](#secret-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables) 1. YAML-defined [global variables](../yaml/README.md#variables) +1. [Deployment variables](#deployment-variables) 1. [Predefined variables](#predefined-variables-environment-variables) (are the lowest in the chain) @@ -40,6 +41,7 @@ version of Runner required. | **CI_BUILD_NAME** | all | 0.5 | The name of the build as defined in `.gitlab-ci.yml` | | **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | | **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built | +| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | | **CI_BUILD_REPO** | all | all | The URL to clone the Git repository | | **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that build was [triggered] | | **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build was manually started | @@ -51,12 +53,17 @@ version of Runner required. | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run | +| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build | +| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | | **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | | **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | +| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a build | +| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a build | +| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a build | | **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build | | **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build | @@ -145,6 +152,20 @@ Secret variables can be added by going to your project's Once you set them, they will be available for all subsequent builds. +## Deployment variables + +>**Note:** +This feature requires GitLab CI 8.15 or higher. + +[Project services](../../project_services/project_services.md) that are +responsible for deployment configuration may define their own variables that +are set in the build environment. These variables are only defined for +[deployment builds](../environments.md). Please consult the documentation of +the project services that you are using to learn which variables they define. + +An example project service that defines deployment variables is +[Kubernetes Service](../../project_services/kubernetes.md). + ## Debug tracing > Introduced in GitLab Runner 1.7. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5f88974d36001ba1dc8b8c070f268770e7042f7f..7158b2e7895619241bf71dddcd79b2443eddc945 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -690,18 +690,12 @@ The `stop_review_app` job is **required** to have the following keywords defined #### dynamic environments > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. + `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15 `environment` can also represent a configuration hash with `name` and `url`. These parameters can use any of the defined [CI variables](#variables) (including predefined, secure variables and `.gitlab-ci.yml` variables). ->**Note:** -Be aware than if the branch name contains special characters and you use the -`$CI_BUILD_REF_NAME` variable to dynamically create environments, there might -be complications during deployment. Follow the -[issue 22849](https://gitlab.com/gitlab-org/gitlab-ce/issues/22849) for more -information. - For example: ``` @@ -709,15 +703,17 @@ deploy as review app: stage: deploy script: make deploy environment: - name: review-apps/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_NAME.review.example.com/ + name: review/$CI_BUILD_REF_NAME + url: https://$CI_ENVIRONMENT_SLUG.example.com/ ``` The `deploy as review app` job will be marked as deployment to dynamically -create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME` -is an [environment variable][variables] set by the Runner. If for example the -`deploy as review app` job was run in a branch named `pow`, this environment -should be accessible under `https://pow.review.example.com/`. +create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME` +is an [environment variable][variables] set by the Runner. The +`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable +for inclusion in URLs. In this case, if the `deploy as review app` job was run +in a branch named `pow`, this environment would be accessible with an URL like +`https://review-pow-aaaaaa.example.com/`. This of course implies that the underlying server which hosts the application is properly configured. @@ -1038,6 +1034,31 @@ variables: GIT_STRATEGY: none ``` +## Build stages attempts + +> Introduced in GitLab, it requires GitLab Runner v1.9+. + +You can set the number for attempts the running build will try to execute each +of the following stages: + +| Variable | Description | +|-------------------------|-------------| +| **GET_SOURCES_ATTEMPTS** | Number of attempts to fetch sources running a build | +| **ARTIFACT_DOWNLOAD_ATTEMPTS** | Number of attempts to download artifacts running a build | +| **RESTORE_CACHE_ATTEMPTS** | Number of attempts to restore the cache running a build | + +The default is one single attempt. + +Example: + +``` +variables: + GET_SOURCES_ATTEMPTS: "3" +``` + +You can set them in the global [`variables`](#variables) section or the [`variables`](#job-variables) +section for individual jobs. + ## Shallow cloning > Introduced in GitLab 8.9 as an experimental feature. May change in future @@ -1246,3 +1267,4 @@ CI with various languages. [environment]: ../environments.md [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 [variables]: ../variables/README.md +[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 33fd50f4c11d30423a08900a0668daa0a9584c2c..4eb7a8eee4887cd164198ee3f2410156637484ae 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -6,7 +6,7 @@ There are two editions of GitLab: [Enterprise Edition](https://about.gitlab.com/ EE releases are available not long after CE releases. To obtain the GitLab EE there is a [repository at gitlab.com](https://gitlab.com/subscribers/gitlab-ee). For more information about the release process see the section 'New versions and upgrading' in the readme. -Both EE and CE require an add-on component called gitlab-shell. It is obtained from the [gitlab-shell repository](https://gitlab.com/gitlab-org/gitlab-shell/tree/master). New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical. +Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical. ## Physical office analogy @@ -35,8 +35,10 @@ Their job description: - make tasks for Sidekiq; - fetch stuff from the warehouse or move things around in there; -**Gitlab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP). -Gitlab-shell communicates with Sidekiq via the “communication board†(Redis), and asks quick questions of the Unicorn workers either directly or via the front desk. +**GitLab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP). +GitLab-shell communicates with Sidekiq via the “communication board†(Redis), and asks quick questions of the Unicorn workers either directly or via the front desk. + +**Gitaly** is a back desk that is specialized on reaching the disks to perform git operations efficiently and keep a copy of the result of costly operations. All git operations go through Gitaly. **GitLab Enterprise Edition (the application)** is the collection of processes and business practices that the office is run by. @@ -53,7 +55,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell ### Components  - + _[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_ A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. @@ -62,7 +64,9 @@ The GitLab web app uses MySQL or PostgreSQL for persistent database information When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects. -The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories directly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. +The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. + +Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files) ### Installation Folder Summary diff --git a/doc/development/changelog.md b/doc/development/changelog.md index 6a97fae9cac9ca287b84114e39704ce1bd3503a5..c71858c6a2450b3f706002e0271fb7cccfd9250c 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -40,6 +40,15 @@ Its simplest usage is to provide the value for `title`: ```text $ bin/changelog 'Hey DZ, I added a feature to GitLab!' +``` + +The entry filename is based on the name of the current Git branch. If you run +the command above on a branch called `feature/hey-dz`, it will generate a +`changelogs/unreleased/feature-hey-dz.yml` file. + +The command will output the path of the generated file and its contents: + +```text create changelogs/unreleased/my-feature.yml --- title: Hey DZ, I added a feature to GitLab! @@ -47,10 +56,6 @@ merge_request: author: ``` -The entry filename is based on the name of the current Git branch. If you run -the command above on a branch called `feature/hey-dz`, it will generate a -`changelogs/unreleased/feature-hey-dz.yml` file. - ### Arguments | Argument | Shorthand | Purpose | @@ -139,7 +144,7 @@ Use the **`--git-username`** or **`-u`** argument to automatically fill in the $ git config user.name Jane Doe -$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!' +$ bin/changelog -u 'Hey DZ, I added a feature to GitLab!' create changelogs/unreleased/feature-hey-dz.yml --- title: Hey DZ, I added a feature to GitLab! diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png index cda5ce254ce478b33c9848894bcbd55806ed2281..378f73845744220d25af57dbcf21382ff658b55a 100644 Binary files a/doc/development/gitlab_architecture_diagram.png and b/doc/development/gitlab_architecture_diagram.png differ diff --git a/doc/development/performance.md b/doc/development/performance.md index 5c43ae7b79ae26cf9bf9cd26480556ae3b80ff5f..f936a49a2aa6210f25f1264b47d7c2a8b6acc934 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -37,7 +37,7 @@ graphs/dashboards. GitLab provides built-in tools to aid the process of improving performance: * [Sherlock](profiling.md#sherlock) -* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md) +* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md) * [Request Profiling](../administration/monitoring/performance/request_profiling.md) GitLab employees can use GitLab.com's performance monitoring systems located at diff --git a/doc/development/sidekiq_debugging.md b/doc/development/sidekiq_debugging.md index cea11e5f126601e31c1886de25792f2b77461150..d6d770e27c12ac653bbffb013cabeea6022f6db4 100644 --- a/doc/development/sidekiq_debugging.md +++ b/doc/development/sidekiq_debugging.md @@ -3,12 +3,15 @@ ## Log arguments to Sidekiq jobs If you want to see what arguments are being passed to Sidekiq jobs you can set -the SIDEKIQ_LOG_ARGUMENTS environment variable. +the `SIDEKIQ_LOG_ARGUMENTS` [environment variable] +(https://docs.gitlab.com/omnibus/settings/environment-variables.html) to `1` (true). + +Example: ``` -SIDEKIQ_LOG_ARGUMENTS=1 bundle exec foreman start +gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"} ``` -It is not recommend to enable this setting in production because some Sidekiq -jobs (such as sending a password reset email) take secret arguments (for -example the password reset token). +Please note: It is not recommend to enable this setting in production because some +Sidekiq jobs (such as sending a password reset email) take secret arguments (for +example the password reset token). \ No newline at end of file diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index daeb15460c2ce83b5ce3f1fe7e58c659dd5d761b..903e54bf9dc1cccf0b5031f906fc55629e998fb6 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -39,4 +39,19 @@ When information is updating in place, a quick, subtle animation is needed. The  -> TODO: Add guidance for other kinds of animation \ No newline at end of file +### Moving transitions + +When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`. + +#### Shifting elements on reorder +An example of a moving transition is when elements have to move out of the way when you drag an element. + +View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here. + + + +#### Autoscroll the page +Another example of a moving transition is when you have to autoscroll the page to keep an active element visible. + +View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here. + \ No newline at end of file diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index 76b739386a598eafa3fbc9a67921efa31c43e0bf..259b214bd59fa8ea22a983ac56402a781c0e2478 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -5,6 +5,7 @@ * [Typography](#typography) * [Icons](#icons) * [Color](#color) +* [Cursors](#cursors) --- @@ -49,13 +50,35 @@ GitLab uses Font Awesome icons throughout our interface. ## Color +| | State | Action | +| :------: | :------- | :------- | +|  | Primary and active (such as the current tab) | Organizational, managing, and retry commands| +|  | Opened | Create new objects | +|  | Warning | Non destructive action | +|  | Closed | Delete and other destructive commands | +|  | Neutral | Neutral secondary commands | + +### Text colors + +||| +| :---: | :--- | +|  | Used for primary body text, such as issue description and comment | +|  | Used for secondary body text, such as username and date | + +> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. + +--- + +## Cursors +The mouse cursor is key in helping users understand how to interact with elements on the screen. + | | | | :------: | :------- | -|  | Blue is used to highlight primary active elements (such as the current tab), as well as other organizational and managing commands.| -|  | Green is for actions that create new objects. | -|  | Orange is used for warnings | -|  | Red is reserved for delete and other destructive commands | -|  | Grey is used for neutral secondary elements. Depending on context, white is sometimes used instead. | +|  | Default cursor | +|  | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons | +|  | Move cursor: used to indicate that you can move an element around on the screen | +|  | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. | +|  | Pan cursor (closed): indicates that you are actively panning the canvas. | +|  | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. | -> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 28383ad7dfc7bffc890b4cf073e1b3fc966d4de3..706bb180912306ba4df513961b0ba0a8a6cd07e6 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -12,6 +12,7 @@ * [Panels](#panels) * [Alerts](#alerts) * [Forms](#forms) +* [Search box](#search-box) * [File holders](#file-holders) * [Data formats](#data-formats) @@ -215,6 +216,18 @@ Horizontal form (`form.horizontal-form`) with label rendered inline with input. --- +## Search box + +Search boxes across GitLab have a consistent rest, active and text entered state. When text isn't entered in the box, there should be a magnifying glass right aligned with the input field. When text is entered, the magnifying glass should become a x, allowing users to clear their text. + + + +If needed, we indicate the scope of the search in the search box. + + + +--- + ## File holders A file holder (`.file-holder`) is used to show the contents of a file inline on a page of GitLab. diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index 8896d450f14cb35c6896e9aed453c2f51ef4fb61..31cc9dd2a534793fbdd6245b3b2b2a7e48bc4acd 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -2,14 +2,16 @@ The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding. -The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab. +The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab. + +Portions of this page are inspired by work found in the [Material Design guidelines][material design]. >**Note:** We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align. ## Contents * [Brevity](#brevity) -* [Sentence case](#sentence-case) +* [Capitalization and punctuation](#capitalization-and-punctuation) * [Terminology](#terminology) --- @@ -29,8 +31,71 @@ Preferrably use context and placement of controls to make it obvious what clicki --- -## Sentence case -Use sentence case for all titles, headings, labels, menu items, and buttons. +## Capitalization and punctuation + +### Case +Use sentence case for titles, headings, labels, menu items, and buttons. Use title case when referring to [features][features] or [products][products]. Note that some features are also objects (e.g. “Merge Requests†and “merge requestsâ€). + +| :white_check_mark: Do | :no_entry_sign: Don’t | +| --- | --- | +| Add issues to the Issue Board feature in GitLab Hosted | Add Issues To The Issue Board Feature In GitLab Hosted | + +### Avoid periods +Avoid using periods in solitary sentences in these elements: + +* Labels +* Hover text +* Bulleted lists +* Dialog body text + +Periods should be used for: + +* Lists or dialogs with multiple sentences +* Any sentence followed by a link + +| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence | +| --- | --- | +| Mention someone to notify them. [Learn more](#) | Mention someone to notify them. [Learn more](#). | + +| :white_check_mark: **Do** skip periods after solo sentences of body text | :no_entry_sign: **Don’t** place periods after body text if there is only a single sentence | +| --- | --- | +| To see the available commands, enter `/gitlab help` | To see the available commands, enter `/gitlab help`. | + +### Use contractions +Don’t make a sentence harder to understand just to follow this rule. For example, “do not†can give more emphasis than “don’t†when needed. + +| :white_check_mark: Do | :no_entry_sign: Don’t | +| --- | --- | +| it’s, can’t, wouldn’t, you’re, you’ve, haven’t, don’t | it is, cannot, would not, it’ll, should’ve | + +### Use numerals for numbers +Use “1, 2, 3†instead of “one, two, three†for numbers. One exception is when mixing uses of numbers, such as “Enter two 3s.†+ +| :white_check_mark: Do | :no_entry_sign: Don’t | +| --- | --- | +| 3 new commits | Three new commits | + +### Punctuation +Omit punctuation after phrases and labels to create a cleaner and more readable interface. Use punctuation to add clarity or be grammatically correct. + +| Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description | +|---|---|---|---|---|---|---| +| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. | +| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. | +| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. | +| Colon | **:** | `:` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. | +| Apostrophe | **’** | `’` | `\u2019` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use for contractions (I’m, you’re, ’89) and to show possession.<br><br>To show possession, add an *’s* to all single nouns and names, even if they already end in an *s*: “Your issues’s status was changed.†For singular proper names ending in *s*, use only an apostrophe: “James’ commits.†For plurals of a single letter, add an *’s*: “Dot your i’s and cross your t’s.â€<br><br>Omit for decades or acronyms: “the 1990sâ€, “MRs.†| +| Quotation marks | **“**<br><br>**â€**<br><br>**‘**<br><br>**’** | `“`<br><br>`”`<br><br>`‘`<br><br>`’` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. | +| Primes | **′**<br><br>**″** | `′`<br><br>`″` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. | +| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `"`<br><br>`'`<br><br>```<br><br>`´` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. | +| Ellipsis | **…** | `…` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…â€) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. | +| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `«`<br><br>`»`<br><br>`‹`<br><br>`›`<br><br>`<`<br><br>`>` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. | +| Em dash | **—** | `—` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. | +| En dash | **–** | `–` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kgâ€, “8:00 AM–12:30 PMâ€, “10–17 Jan†| +| Hyphen | **-** | | | | | Use to represent negative numbers, or to avoid ambiguity in adjective-noun or noun-participle pairs. Example: “anti-inflammatoryâ€; “5-mile walk.â€<br><br>Omit in commonly understood terms and adverbs that end in *ly*: “frontendâ€, “greatly improved performance.â€<br><br>Omit in the term “open source.†| +| Parentheses | **( )** | | | | | Use only to define acronyms or jargon: “Secure web connections are based on a technology called SSL (the secure sockets layer).â€<br><br>Avoid other uses and instead rewrite the text, or use dashes or commas to set off the information. If parentheses are required: If the parenthetical is a complete, independent sentence, place the period inside the parentheses; if not, the period goes outside. | + +When using the <kbd>Alt</kbd> keystrokes in Windows, use the numeric keypad, not the row of numbers above the alphabet, and be sure Num Lock is turned on. --- @@ -48,15 +113,15 @@ Only use the terms in the tables below. | Deleted | >**Example:** -Use `5 open issues` and don't use `5 pending issues`. +Use `5 open issues` and don’t use `5 pending issues`. #### Verbs (actions) -| Term | Use | Don't | +| Term | Use | Don’t | | ---- | --- | --- | -| Add | Add an issue | Don't use `create` or `new` | +| Add | Add an issue | Don’t use `create` or `new` | | View | View an open or closed issue || -| Edit | Edit an open or closed issue | Don't use `update` | +| Edit | Edit an open or closed issue | Don’t use `update` | | Close | Close an open issue || | Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb | | Delete | Delete an open or closed issue || @@ -67,7 +132,7 @@ When viewing a list of issues, there is a button that is labeled `Add`. Given th  -The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don't use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`. +The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don’t use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.  @@ -77,7 +142,7 @@ When in context of an issue, the affordance to edit it is labeled `Edit`. If the  -The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don't use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`. +The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don’t use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.  @@ -93,7 +158,7 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav #### Verbs (actions) -| Term | Use | Don't | +| Term | Use | Don’t | | ---- | --- | --- | | Add | Add a merge request | Do not use `create` or `new` | | View | View an open or merged merge request || @@ -101,3 +166,22 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav | Approve | Approve an open merge request || | Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word| | Merge | Merge an open merge request || + +### Comments & Discussions + +#### Comment +A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and timestamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions. + +#### Discussion +A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved. + +--- + +Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons]. + +[material design]: https://material.io/guidelines/ +[features]: https://about.gitlab.com/features/ "GitLab features page" +[products]: https://about.gitlab.com/products/ "GitLab products page" +[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma†in Wikipedia" +[android project]: http://source.android.com/ +[creative commons]: http://creativecommons.org/licenses/by/2.5/ \ No newline at end of file diff --git a/doc/development/ux_guide/img/animation-autoscroll.gif b/doc/development/ux_guide/img/animation-autoscroll.gif new file mode 100644 index 0000000000000000000000000000000000000000..155b0234c6463c7728020d101bae0e67e1d89dac Binary files /dev/null and b/doc/development/ux_guide/img/animation-autoscroll.gif differ diff --git a/doc/development/ux_guide/img/animation-reorder.gif b/doc/development/ux_guide/img/animation-reorder.gif new file mode 100644 index 0000000000000000000000000000000000000000..ccdeb3d396f68179407081384e846b82698dc826 Binary files /dev/null and b/doc/development/ux_guide/img/animation-reorder.gif differ diff --git a/doc/development/ux_guide/img/color-textprimary.png b/doc/development/ux_guide/img/color-textprimary.png new file mode 100644 index 0000000000000000000000000000000000000000..90f2821f0cf335a356deec38967e69db31725c8f Binary files /dev/null and b/doc/development/ux_guide/img/color-textprimary.png differ diff --git a/doc/development/ux_guide/img/color-textsecondary.png b/doc/development/ux_guide/img/color-textsecondary.png new file mode 100644 index 0000000000000000000000000000000000000000..61cb8a13c45a7b0d918f0aa6a01b2f1ca7ff461b Binary files /dev/null and b/doc/development/ux_guide/img/color-textsecondary.png differ diff --git a/doc/development/ux_guide/img/components-searchbox.png b/doc/development/ux_guide/img/components-searchbox.png new file mode 100644 index 0000000000000000000000000000000000000000..a25189296ba6b4990bb4a2d9ebca51938f73216d Binary files /dev/null and b/doc/development/ux_guide/img/components-searchbox.png differ diff --git a/doc/development/ux_guide/img/components-searchboxscoped.png b/doc/development/ux_guide/img/components-searchboxscoped.png new file mode 100644 index 0000000000000000000000000000000000000000..b116d714848ccfc49ba7b3478e703c96a640ce06 Binary files /dev/null and b/doc/development/ux_guide/img/components-searchboxscoped.png differ diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png new file mode 100644 index 0000000000000000000000000000000000000000..c188ec4e3512da6b03c0fc9cd8463c60e11af9ff Binary files /dev/null and b/doc/development/ux_guide/img/cursors-default.png differ diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png new file mode 100644 index 0000000000000000000000000000000000000000..86f286399826d632d6f8e1b9f7f26ac2620dc8e7 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-ibeam.png differ diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png new file mode 100644 index 0000000000000000000000000000000000000000..a9c610eaa882105d4011de7ba220fa0e4256a207 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-move.png differ diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png new file mode 100644 index 0000000000000000000000000000000000000000..6d247a765aca5cf4f4412d607c12c868c688e848 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panclosed.png differ diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png new file mode 100644 index 0000000000000000000000000000000000000000..76f2eeda8316cdf3764aeeb48d6c0eab139bceb2 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panopened.png differ diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..d86dd955fa78b158255739595a50def06f5d31dc Binary files /dev/null and b/doc/development/ux_guide/img/cursors-pointer.png differ diff --git a/doc/install/installation.md b/doc/install/installation.md index 2740b2982b9c62ed8b2e5cec8c2685a3366c5c1f..9cebed34b7e2f2e8a0012941d0a0a2f5bbaead0e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -271,9 +271,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-16-stable gitlab -**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-16-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -400,16 +400,10 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse` which is the recommended location. - cd /home/git/gitlab - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production ### Initialize Database and Activate Advanced Features - # Go to GitLab installation folder - - cd /home/git/gitlab - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production # Type 'yes' to create the database tables. @@ -607,6 +601,12 @@ If you want to connect the Redis server via socket, then use the "unix:" URL sch production: url: unix:/path/to/redis/socket +Also you can use environment variables in the `config/resque.yml` file: + + # example + production: + url: <%= ENV.fetch('GITLAB_REDIS_URL') %> + ### Custom SSH Connection If you are running SSH on a non-standard port, you must change the GitLab user's SSH config. diff --git a/doc/integration/README.md b/doc/integration/README.md index f8ffa6dcb7f88ee888f1f01e23e1494487b0edd9..e97430feb573a7b4ee2cc58511f3cd504c5973a8 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,7 +8,7 @@ See the documentation below for details on how to configure these services. - [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [OAuth2 provider](oauth_provider.md) OAuth2 application creation @@ -16,6 +16,7 @@ See the documentation below for details on how to configure these services. - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam - [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration +- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 9122dc62e39f984ed9d5e16b7d3fb5edc93a7ede..2a14c0397ca91d960ffc7b5bf22d6d77e2220cc1 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -5,7 +5,7 @@ Bitbucket.org account. ## Overview -You can set up Bitbucket.org as an OAuth provider so that you can use your +You can set up Bitbucket.org as an OAuth2 provider so that you can use your credentials to authenticate into GitLab or import your projects from Bitbucket.org. @@ -18,8 +18,10 @@ Bitbucket.org. ## Bitbucket OmniAuth provider > **Note:** -Make sure to first follow the [Initial OmniAuth configuration][init-oauth] -before proceeding with setting up the Bitbucket integration. +GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with +GitLab. You are encouraged to upgrade your GitLab instance if you haven't done +already. If you're using GitLab 8.14 and below, [use the previous integration +docs][bb-old]. To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.org. Bitbucket will generate an application ID and secret key for @@ -38,20 +40,23 @@ you to use. | :--- | :---------- | | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. | | **Application description** | Fill this in if you wish. | - | **Callback URL** | Leave blank. | + | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | + NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will + see an "Invalid redirect_uri" message. For more details, see [the + Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html). + And grant at least the following permissions: ``` - Account: Email - Repositories: Read, Admin + Account: Email, Read + Repositories: Read + Pull Requests: Read + Issues: Read + Wiki: Read and Write ``` - >**Note:** - It may seem a little odd to giving GitLab admin permissions to repositories, - but this is needed in order for GitLab to be able to clone the repositories. -  1. Select **Save**. @@ -93,7 +98,8 @@ you to use. ```yaml - { name: 'bitbucket', app_id: 'BITBUCKET_APP_KEY', - app_secret: 'BITBUCKET_APP_SECRET' } + app_secret: 'BITBUCKET_APP_SECRET', + url: 'https://bitbucket.org/' } ``` --- @@ -112,100 +118,12 @@ well, the user will be returned to GitLab and will be signed in. ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires two -extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md). - -Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and -instead requires GitLab to use SSH and identify itself using your GitLab -server's SSH key. - -To be able to access repositories on Bitbucket, GitLab will automatically -register your public key with Bitbucket as a deploy key for the repositories to -be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which -translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to -`/home/git/.ssh/bitbucket_rsa` for installations from source. - ---- - -Below are the steps that will allow GitLab to be able to import your projects -from Bitbucket. - -1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider). -1. Create a new SSH key with an **empty passphrase**: - - ```sh - sudo -u git -H ssh-keygen - ``` - - When asked to 'Enter file in which to save the key' enter: - `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or - `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is - important so make sure to get it right. - - > **Warning:** - This key must NOT be associated with ANY existing Bitbucket accounts. If it - is, the import will fail with an `Access denied! Please verify you can add - deploy keys to this repository.` error. - -1. Next, you need to to configure the SSH client to use your new key. Open the - SSH configuration file of the `git` user: - - ``` - # For Omnibus packages - sudo editor /var/opt/gitlab/.ssh/config - - # For installations from source - sudo editor /home/git/.ssh/config - ``` - -1. Add a host configuration for `bitbucket.org`: - - ```sh - Host bitbucket.org - IdentityFile ~/.ssh/bitbucket_rsa - User git - ``` - -1. Save the file and exit. -1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git` - user that GitLab will use: - - ```sh - sudo -u git -H ssh bitbucket.org - ``` - - That step is performed because GitLab needs to connect to Bitbucket over SSH, - in order to add `bitbucket.org` to your GitLab server's known SSH hosts. - -1. Verify the RSA key fingerprint you'll see in the response matches the one - in the [Bitbucket documentation][bitbucket-docs] (the specific IP address - doesn't matter): - - ```sh - The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established. - RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A. - Are you sure you want to continue connecting (yes/no)? - ``` - -1. If the fingerprint matches, type `yes` to continue connecting and have - `bitbucket.org` be added to your known SSH hosts. After confirming you should - see a permission denied message. If you see an authentication successful - message you have done something wrong. The key you are using has already been - added to a Bitbucket account and will cause the import script to fail. Ensure - the key you are using CANNOT authenticate with Bitbucket. -1. Restart GitLab to allow it to find the new public key. - -Your GitLab server is now able to connect to Bitbucket over SSH. You should be -able to see the "Import projects from Bitbucket" option on the New Project page -enabled. - -## Acknowledgements - -Special thanks to the writer behind the following article: - -- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/ +Once the above configuration is set up, you can use Bitbucket to sign into +GitLab and [start importing your projects][bb-import]. [init-oauth]: omniauth.md#initial-omniauth-configuration +[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md +[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md [bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/integration/chat_commands.md b/doc/integration/chat_commands.md new file mode 100644 index 0000000000000000000000000000000000000000..4b0084678d91afe8b95dc31cff4a37dc01c9f93c --- /dev/null +++ b/doc/integration/chat_commands.md @@ -0,0 +1,14 @@ +# Chat Commands + +Chat commands allow user to perform common operations on GitLab right from there chat client. +Right now both Mattermost and Slack are supported. + +## Available commands + +The trigger is configurable, but for the sake of this example, we'll use `/trigger` + +* `/trigger help` - Displays all available commands for this user +* `/trigger issue new <title> <shift+return> <description>` - creates a new issue on the project +* `/trigger issue show <id>` - Shows the issue with the given ID, if you've got access +* `/trigger issue search <query>` - Shows a maximum of 5 items matching the query +* `/trigger deploy <from> to <to>` - Deploy from an environment to another diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png index 8dbee9762d7510f9e553643c61dd52a353ebbc39..3e6dea6cfe9abfe51777318f07f916e6d2806996 100644 Binary files a/doc/integration/img/bitbucket_oauth_settings_page.png and b/doc/integration/img/bitbucket_oauth_settings_page.png differ diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 8a55fce96fe2f6952abecbabf0f9784d5b0d33e3..4c933cef9b7cc957a7a225588663c8c2615a3a8f 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -30,6 +30,7 @@ contains some settings that are common for all providers. - [Crowd](crowd.md) - [Azure](azure.md) - [Auth0](auth0.md) +- [Authentiq](../administration/auth/authentiq.md) ## Initial OmniAuth Configuration diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..349a2dc8456e405b129611a53aacc8af41e59d6c Binary files /dev/null and b/doc/project_services/img/kubernetes_configuration.png differ diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..3c5ff5ee317ff47596210461924bdd635faeaab1 Binary files /dev/null and b/doc/project_services/img/mattermost_configuration.png differ diff --git a/doc/project_services/img/slack_setup.png b/doc/project_services/img/slack_setup.png new file mode 100644 index 0000000000000000000000000000000000000000..f69817f2b78d6621915fcb1b0cc61359e7fe812d Binary files /dev/null and b/doc/project_services/img/slack_setup.png differ diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md new file mode 100644 index 0000000000000000000000000000000000000000..59d5da702f81e2d14024cd31fde01a37f1eba84a --- /dev/null +++ b/doc/project_services/kubernetes.md @@ -0,0 +1,63 @@ +# GitLab Kubernetes / OpenShift integration + +GitLab can be configured to interact with Kubernetes, or other systems using the +Kubernetes API (such as OpenShift). + +Each project can be configured to connect to a different Kubernetes cluster, see +the [configuration](#configuration) section. + +If you have a single cluster that you want to use for all your projects, +you can pre-fill the settings page with a default template. To configure the +template, see the [Services Templates](services-templates.md) document. + +## Configuration + + + +The Kubernetes service takes the following arguments: + +1. Kubernetes namespace +1. API URL +1. Service token +1. Custom CA bundle + +The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes +exposes several APIs - we want the "base" URL that is common to all of them, +e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. + +GitLab authenticates against Kubernetes using service tokens, which are +scoped to a particular `namespace`. If you don't have a service token yet, +you can follow the +[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) +to create one. You can also view or create service tokens in the +[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit +`Config -> Secrets`. + +Fill in the service token and namespace according to the values you just got. +If the API is using a self-signed TLS certificate, you'll also need to include +the `ca.crt` contents as the `Custom CA bundle`. + +## Deployment variables + +The Kubernetes service exposes following +[deployment variables](../ci/variables/README.md#deployment-variables) in the +GitLab CI build environment: + +- `KUBE_URL` - equal to the API URL +- `KUBE_TOKEN` +- `KUBE_NAMESPACE` +- `KUBE_CA_PEM` - only if a custom CA bundle was specified + +## Web terminals + +>**NOTE:** +Added in GitLab 8.15. You must be the project owner or have `master` permissions +to use terminals. Support is currently limited to the first container in the +first pod of your environment. + +When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) +support to your environments. This is based on the `exec` functionality found in +Docker and Kubernetes, so you get a new shell session within your existing +containers. To use this integration, you should deploy to Kubernetes using +the deployment variables above, ensuring any pods you create are labelled with +`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md new file mode 100644 index 0000000000000000000000000000000000000000..fbc7dfeee6d0637cb01ad4fcada6af1cd675bc39 --- /dev/null +++ b/doc/project_services/mattermost.md @@ -0,0 +1,45 @@ +# Mattermost Notifications Service + +## On Mattermost + +To enable Mattermost integration you must create an incoming webhook integration: + +1. Sign in to your Mattermost instance +1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add +1. Choose a display name, description and channel, those can be overridden on GitLab +1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. + +There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable +it on https://mattermost.example/admin_console/integrations/custom. + +Display name override is not enabled by default, you need to ask your admin to enable it on that same section. + +## On GitLab + +After you set up Mattermost, it's time to set up GitLab. + +Go to your project's **Settings > Services > Mattermost Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Mattermost channel you want to send that event message, with `#town-square` +being the default. The hash sign is optional. + +At the end, fill in your Mattermost details: + +| Field | Description | +| ----- | ----------- | +| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | +| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + + + diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 6fcbf6f1642c145eba773491767742641f0cba44..67cb88104c134ea1c904b3b0054a6deb22e622bf 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -14,12 +14,18 @@ If you have the Omnibus GitLab package installed, Mattermost is already bundled in it. All you have to do is configure it. Read more in the [Omnibus GitLab Mattermost documentation][omnimmdocs]. -## Configuration +## Automated Configuration + +If Mattermost is installed on the same server as GitLab, the configuration process can be +done for you by GitLab. + +Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. + +## Manual Configuration The configuration consists of two parts. First you need to enable the slash commands in Mattermost and then enable the service in GitLab. - ### Step 1. Enable custom slash commands in Mattermost This step is only required when using a source install, omnibus installs will be @@ -65,7 +71,7 @@ the administrator console. ### Step 3. Create a new custom slash command in Mattermost -Now that you have enabled the custom slash commands in Mattermost and opened +Now that you have enabled custom slash commands in Mattermost and opened the Mattermost slash commands service in GitLab, it's time to copy these values in a new slash command. @@ -128,20 +134,16 @@ GitLab using the Mattermost commands. ## Available slash commands -The available slash commands so far are: +The available slash commands are: | Command | Description | Example | | ------- | ----------- | ------- | -| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` | -| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` | -| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` | - -To see a list of available commands that can interact with GitLab, type the -trigger word followed by `help`: +| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | -``` -/my-project help -``` +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>  diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 890f7525b0e745da9f5d48d3e2b2583577bdbbba..0f398874b8fc6db774c9b8b8ca698bd83699ae46 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -42,11 +42,13 @@ further configuration instructions and details. Contributions are welcome. | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | +| [Kubernetes](kubernetes.md) | A containerized deployment service | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | +| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | +| [Slack Notifications](slack.md) | Receive event notifications in Slack | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | -| [Slack](slack.md) | A team communication tool for the 21st century | ## Services Templates diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index 3cfe77c9f851464350afc323ea9e4773e60b0ce9..0b682b43810853a9fd1f868f4162563fa0a42b9e 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,4 +1,4 @@ -# Slack Service +# Slack Notifications Service ## On Slack @@ -15,7 +15,7 @@ Slack: After you set up Slack, it's time to set up GitLab. -Go to your project's **Settings > Services > Slack** and you will see a +Go to your project's **Settings > Services > Slack Notifications** and you will see a checkbox with the following events that can be triggered: - Push diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md new file mode 100644 index 0000000000000000000000000000000000000000..d9ff573d1851ec02a02d17d46b81876706a4da69 --- /dev/null +++ b/doc/project_services/slack_slash_commands.md @@ -0,0 +1,23 @@ +# Slack slash commands + +> Introduced in GitLab 8.15 + +Slack commands give users an extra interface to perform common operations +from the chat environment. This allows one to, for example, create an issue as +soon as the idea was discussed in chat. +For all available commands try the help subcommand, for example: `/gitlab help`, +all review the [full list of commands](../integration/chat_commands.md). + +## Prerequisites + +A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you. + +## Configuration + +First, navigate to the Slack Slash commands service page, found at your project's +**Settings** > **Services**, and you find the instructions there: + +  + +Once you've followed the instructions, mark the service as active and insert the token +you've received from Slack. After saving the service you are good to go! diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index a49c43b8ef2bd0f40c62a657aa1b4e0c36ebc8d4..2b81ebc9c595c66112a5a38e2b4c80e186e093c7 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -4,7 +4,8 @@ - [Check](check.md) - [Cleanup](cleanup.md) - [Features](features.md) -- [Maintenance](maintenance.md) and self-checks +- [LDAP Maintenance](../administration/raketasks/ldap.md) +- [General Maintenance](maintenance.md) and self-checks - [User management](user_management.md) - [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index f42bb6a81a235a7ecb694a9d7ea8193d2195588a..f6b4db71b44755860ccba41725f85b3ba868eb4b 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -9,6 +9,9 @@ This archive will be saved in `backup_path`, which is specified in the The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP` identifies the time at which each backup was created. +> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`) +> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`) + You can only restore a backup to exactly the same version of GitLab on which it was created. The best way to migrate your repositories from one server to another is through backup restore. @@ -88,10 +91,10 @@ It uses the [Fog library](http://fog.io/) to perform the upload. In the example below we use Amazon S3 for storage, but Fog also lets you use [other storage providers](http://fog.io/storage/). GitLab [imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88) -for AWS, OpenStack Swift and Rackspace as well. A local driver is +for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is [also available](#uploading-to-locally-mounted-shares). -For omnibus packages: +For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`: ```ruby gitlab_rails['backup_upload_connection'] = { @@ -106,6 +109,8 @@ gitlab_rails['backup_upload_connection'] = { gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' ``` +Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes. + For installations from source: ```yaml @@ -223,7 +228,8 @@ For installations from source: ## Backup archive permissions -The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default. +The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`) +will have owner/group git:git and 0600 permissions by default. This is meant to avoid other system users reading GitLab's data. If you need the backup archives to have different permissions you can use the 'archive_permissions' setting. @@ -335,7 +341,7 @@ First make sure your backup tar file is in the backup directory described in the `/var/opt/gitlab/backups`. ```shell -sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ +sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/ ``` Stop the processes that are connected to the database. Leave the rest of GitLab diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index a0e895773ce002c146480334ab2f3a25c2dff376..c64d34074616c41898567440189d47bd4795a1ce 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-14-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.0 +sudo -u git -H git checkout v4.0.3 ``` ### 6. Update gitlab-workhorse diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 5556dae2551e321dd6c891bb5dddcd003c5ee14e..b1e3b116548d1741c243fb96b2fccf699f67e6fa 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -11,12 +11,15 @@ guide links by version. ### 1. Stop server - sudo service gitlab stop +```bash +sudo service gitlab stop +``` ### 2. Backup ```bash cd /home/git/gitlab + sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` @@ -49,6 +52,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` @@ -56,6 +61,8 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash +cd /home/git/gitlab + sudo -u git -H git checkout 8-15-stable ``` @@ -64,28 +71,12 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 8-15-stable-ee -``` - -### 5. Update gitlab-shell - -```bash -cd /home/git/gitlab-shell -sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.3 -``` - -### 6. Update gitlab-workhorse - -Install and compile gitlab-workhorse. This requires -[Go 1.5](https://golang.org/dl) which should already be on your system from -GitLab 8.1. +cd /home/git/gitlab -```bash -sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +sudo -u git -H git checkout 8-15-stable-ee ``` -### 7. Install libs, migrations, etc. +### 5. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -106,6 +97,27 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v4.1.1 +``` + ### 8. Update configuration files #### New configuration options for `gitlab.yml` @@ -113,6 +125,8 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: ```sh +cd /home/git/gitlab + git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example ``` @@ -122,6 +136,8 @@ Configure Git to generate packfile bitmaps (introduced in Git 2.0) on the GitLab server during `git gc`. ```sh +cd /home/git/gitlab + sudo -u git -H git config --global repack.writeBitmaps true ``` @@ -130,6 +146,8 @@ sudo -u git -H git config --global repack.writeBitmaps true Ensure you're still up-to-date with the latest NGINX configuration changes: ```sh +cd /home/git/gitlab + # For HTTPS configurations git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl @@ -162,26 +180,42 @@ See [smtp_settings.rb.sample] as an example. Ensure you're still up-to-date with the latest init script changes: - sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` For Ubuntu 16.04.1 LTS: - sudo systemctl daemon-reload +```bash +sudo systemctl daemon-reload +``` ### 9. Start application - sudo service gitlab start - sudo service nginx restart +```bash +sudo service gitlab start +sudo service nginx restart +``` ### 10. Check application status Check if GitLab and its environment are configured correctly: - sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` To make sure you didn't miss anything run a more thorough check: - sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` If all items are green, then congratulations, the upgrade is complete! @@ -196,6 +230,7 @@ database migration (the backup is already migrated to the previous version). ```bash cd /home/git/gitlab + sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production ``` diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md new file mode 100644 index 0000000000000000000000000000000000000000..3d68fe201a7ddac94f5d5279f7546865c159941f --- /dev/null +++ b/doc/update/8.15-to-8.16.md @@ -0,0 +1,237 @@ +# From 8.15 to 8.16 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 8-16-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 8-16-stable-ee +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production +``` + +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v4.1.1 +``` + +### 8. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/8-15-stable:config/gitlab.yml.example origin/8-16-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Configure Git to generate packfile bitmaps (introduced in Git 2.0) on +the GitLab server during `git gc`. + +```sh +cd /home/git/gitlab + +sudo -u git -H git config --global repack.writeBitmaps true +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/8-15-stable:lib/support/nginx/gitlab-ssl origin/8-16-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-15-stable:lib/support/nginx/gitlab origin/8-16-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 9. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 10. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.15) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.14 to 8.15](8.14-to-8.15.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 685972cfb410d40f8d08708d94c93d77a0712834..54d523b59fd16e77f7b5779ce3d097a0a7ea0771 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -14,6 +14,7 @@ user on the database version) ```bash cd /home/git/gitlab + sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` @@ -32,28 +33,13 @@ current version with `cat VERSION`). ```bash cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- Gemfile.lock db/schema.rb sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG ``` -### 3. Update gitlab-shell to the corresponding version - -```bash -cd /home/git/gitlab-shell -sudo -u git -H git fetch -sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -``` - -### 4. Update gitlab-workhorse to the corresponding version - -```bash -cd /home/git/gitlab - -sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production -``` - -### 5. Install libs, migrations, etc. +### 3. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -74,6 +60,23 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` +### 4. Update gitlab-workhorse to the corresponding version + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 5. Update gitlab-shell to the corresponding version + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` +``` + ### 6. Start application ```bash @@ -86,6 +89,8 @@ sudo service nginx restart Check if GitLab and its environment are configured correctly: ```bash +cd /home/git/gitlab + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production ``` diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 4d24eb219769d82827ed63c963a851c8ee253e9d..f64846887219288d314ea6c4b3baea1d2dc2e947 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -319,6 +319,40 @@ Here's a sample video:  +### Math + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math + +It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex]. + +Math written inside ```$``$``` will be rendered inline with the text. + +Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line. + +Example: + + This math is inline $`a^2+b^2=c^2`$. + + This is on a separate line + ```math + a^2+b^2=c^2 + ``` + +Becomes: + +This math is inline $`a^2+b^2=c^2`$. + +This is on a separate line +```math +a^2+b^2=c^2 +``` + +_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ + +>**Note:** +This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual]. + ## Standard Markdown ### Headers @@ -764,3 +798,6 @@ A link starting with a `/` is relative to the wiki root. [markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" +[katex]: https://github.com/Khan/KaTeX "KaTeX website" +[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" +[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" \ No newline at end of file diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 39fe2409a295beb00ad58e3d5d64aa0c7ed4423d..5ada8748d853519e9f1a104650e309b8d8e38a2e 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -33,6 +33,7 @@ The following table depicts the various user permission levels in a project. | See a container registry | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ | +| Use environment terminals | | | | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | Manage/Accept merge requests | | | ✓ | ✓ | ✓ | diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index 86fe52ef4ff278eefc6278af05bdeda898c80d33..62afd8cf2478e8ffefe0d11c53c6d48fbce4828c 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -50,7 +50,7 @@ exception of the staging and production stages, where only data deployed to production are measured. Specifically, if your CI is not set up and you have not defined a `production` -[environment], then you will not have any data for those stages. +or `production/*` [environment], then you will not have any data for those stages. Below you can see in more detail what the various stages of Cycle Analytics mean. @@ -61,7 +61,7 @@ Below you can see in more detail what the various stages of Cycle Analytics mean | Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. | | Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. | | Review | Measures the median time taken to review the merge request, between its creation and until it's merged. | -| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. | +| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. | | Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. | --- @@ -79,10 +79,13 @@ Here's a little explanation of how this works behind the scenes: etc. To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all. -So, if a merge request doesn't close an issue or an issue is not labeled with a -label present in the Issue Board or assigned a milestone or a project has no -`production` environment (for staging and production stages), the Cycle Analytics -dashboard won't present any data at all. +So, the Cycle Analytics dashboard won't present any data: +- For merge requests that do not close an issue. +- For issues not labeled with a label present in the Issue Board. +- For issues not assigned a milestone. +- For staging and production stages, if the project has no `production` or `production/*` + environment. + ## Example workflow diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png new file mode 100644 index 0000000000000000000000000000000000000000..8c7ce215ae0a93029be9784eef842ae7f49e479b Binary files /dev/null and b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png differ diff --git a/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png b/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png new file mode 100644 index 0000000000000000000000000000000000000000..98d632783268778572d3238cefa4af504a1c978c Binary files /dev/null and b/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png differ diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md index f37f1ce4d21886e8a4c4c95d0c84ad01e808c986..d4b85676d190ffde20e559f67c05b46f12e5a8a0 100644 --- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md +++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md @@ -51,13 +51,15 @@ are resolved.  -### Move all unresolved discussions in a merge request to an issue +## Move all unresolved discussions in a merge request to an issue -> [Introduced][ce-7180] (Currently on Backlog) +> [Introduced][ce-7180] in GitLab 8.15. To delegate unresolved discussions to a new issue you can click the link **open an issue to resolve them later**. + + This will prepare an issue with content referring to the merge request and discussions. @@ -66,6 +68,8 @@ discussions. Hitting **Submit issue** will cause all discussions to be marked as resolved and add a note referring to the newly created issue. + + You can now proceed to merge the merge request from the UI. [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 5f6a6c6503e4e72adab1bcf163f987892476db32..a6546cffce2d17097e8da00726507755c4d7a1b8 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -14,6 +14,7 @@ do. |:---------------------------|:-------------| | `/close` | Close the issue or merge request | | `/reopen` | Reopen the issue or merge request | +| `/merge` | Merge (when build succeeds) | | `/title <New title>` | Change title | | `/assign @username` | Assign | | `/unassign` | Remove assignee | @@ -29,3 +30,7 @@ do. | <code>/due <in 2 days | this Friday | December 31st></code> | Set due date | | `/remove_due_date` | Remove due date | | `/wip` | Toggle the Work In Progress status | +| <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | +| `/remove_estimate` | Remove estimated time | +| <code>/spend <1h 30m | -1h 5m></code> | Add or substract spent time | +| `/remove_time_spent` | Remove time spent | diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 59a806de210f14d3985d23fc13522c4eb037b48e..b317bd79dedeffe2b0a34f1aeed320eb4074f258 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -19,6 +19,7 @@ - [Slash commands](../user/project/slash_commands.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) +- [Time tracking](time_tracking.md) - [Web Editor](../user/project/repository/web_editor.md) - [Releases](releases.md) - [Milestones](milestones.md) diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md index 18e5d950866a4c16f70a5155848bce9b74f0f80d..2d91bee0e94c48612d6837d2e2aeff17bc2f8602 100644 --- a/doc/workflow/importing/README.md +++ b/doc/workflow/importing/README.md @@ -4,6 +4,7 @@ 1. [GitHub](import_projects_from_github.md) 1. [GitLab.com](import_projects_from_gitlab_com.md) 1. [FogBugz](import_projects_from_fogbugz.md) +1. [Gitea](import_projects_from_gitea.md) 1. [SVN](migrating_from_svn.md) In addition to the specific migration documentation above, you can import any @@ -14,4 +15,3 @@ repository is too large the import can timeout. You can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported. - diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png deleted file mode 100644 index df55a081803b646c89a09205c4fd16bf141788bd..0000000000000000000000000000000000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png and /dev/null differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png deleted file mode 100644 index 5253889d251b6c70a375de4f7a6fef494804af11..0000000000000000000000000000000000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png and /dev/null differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png deleted file mode 100644 index ffa87ce5b2eeb8fa3eddc740f70b3f8e3a3fbaa0..0000000000000000000000000000000000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png and /dev/null differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png deleted file mode 100644 index 1a5661de75d556dea016fe86a67ca043a7c2cd93..0000000000000000000000000000000000000000 Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png and /dev/null differ diff --git a/doc/workflow/importing/img/bitbucket_import_grant_access.png b/doc/workflow/importing/img/bitbucket_import_grant_access.png new file mode 100644 index 0000000000000000000000000000000000000000..429904e621d26b0628c32d8e7145e779f77e2c4a Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_grant_access.png differ diff --git a/doc/workflow/importing/img/bitbucket_import_new_project.png b/doc/workflow/importing/img/bitbucket_import_new_project.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed528c2f09bb5d89d7f9c8e0dc34999e25f6906 Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_new_project.png differ diff --git a/doc/workflow/importing/img/bitbucket_import_select_project.png b/doc/workflow/importing/img/bitbucket_import_select_project.png new file mode 100644 index 0000000000000000000000000000000000000000..1bca6166ec8e3f902bfa9f128e31fd84ac1324bb Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_select_project.png differ diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png new file mode 100644 index 0000000000000000000000000000000000000000..a3f603cbd0a5b6ad5a2f303a3b07f9ae05692652 Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png differ diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png deleted file mode 100644 index b23ade4480c4ee2ddd1a9f81cd03a6ba735a8e53..0000000000000000000000000000000000000000 Binary files a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png and /dev/null differ diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png index f50d92669916e0a250083ee0bf792fa16fd9fe50..1ccb38a815e486a44a06d249b0f332982068fc3a 100644 Binary files a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png and b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png differ diff --git a/doc/workflow/importing/img/import_projects_from_new_project_page.png b/doc/workflow/importing/img/import_projects_from_new_project_page.png new file mode 100644 index 0000000000000000000000000000000000000000..97ca30b20874d405b64fb181a54c5395da203542 Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_new_project_page.png differ diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index 520c42162957bf9e1981a53dca79c03ff2d7a9fe..97380bce172756a216fe27c1e0e503cdcec2629d 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -1,26 +1,62 @@ # Import your project from Bitbucket to GitLab -It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md). +Import your projects from Bitbucket to GitLab with minimal effort. -* Sign in to GitLab.com and go to your dashboard +## Overview -* Click on "New project" +>**Note:** +The [Bitbucket integration][bb-import] must be first enabled in order to be +able to import your projects from Bitbucket. Ask your GitLab administrator +to enable this if not already. - +- At its current state, the Bitbucket importer can import: + - the repository description (GitLab 7.7+) + - the Git repository data (GitLab 7.7+) + - the issues (GitLab 7.7+) + - the issue comments (GitLab 8.15+) + - the pull requests (GitLab 8.4+) + - the pull request comments (GitLab 8.15+) + - the milestones (GitLab 8.15+) + - the wiki (GitLab 8.15+) +- References to pull requests and issues are preserved (GitLab 8.7+) +- Repository public access is retained. If a repository is private in Bitbucket + it will be created as private in GitLab as well. -* Click on the "Bitbucket" button - +## How it works -* Grant GitLab access to your Bitbucket account +When issues/pull requests are being imported, the Bitbucket importer tries to find +the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this +to work, the Bitbucket author/assignee should have signed in beforehand in GitLab +and [**associated their Bitbucket account**][social sign-in]. If the user is not +found in GitLab's database, the project creator (most of the times the current +user that started the import process) is set as the author, but a reference on +the issue about the original Bitbucket author is kept. - +The importer will create any new namespaces (groups) if they don't exist or in +the case the namespace is taken, the repository will be imported under the user's +namespace that started the import process. -* Click on the projects that you'd like to import or "Import all projects" +## Importing your Bitbucket repositories - +1. Sign in to GitLab and go to your dashboard. +1. Click on **New project**. -A new GitLab project will be created with your imported data. +  -### Note -Milestones and wiki pages are not imported from Bitbucket. +1. Click on the "Bitbucket" button + +  + +1. Grant GitLab access to your Bitbucket account + +  + +1. Click on the projects that you'd like to import or **Import all projects**. + You can also select the namespace under which each project will be + imported. + +  + +[bb-import]: ../../integration/bitbucket.md +[social sign-in]: ../../user/profile/account/social_sign_in.md diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md new file mode 100644 index 0000000000000000000000000000000000000000..f5746a0fb31773eed935c761da8663787a93f2aa --- /dev/null +++ b/doc/workflow/importing/import_projects_from_gitea.md @@ -0,0 +1,77 @@ +# Import your project from Gitea to GitLab + +Import your projects from Gitea to GitLab with minimal effort. + +## Overview + +>**Note:** +This requires Gitea `v1.0.0` or newer. + +- At its current state, Gitea importer can import: + - the repository description (GitLab 8.15+) + - the Git repository data (GitLab 8.15+) + - the issues (GitLab 8.15+) + - the pull requests (GitLab 8.15+) + - the milestones (GitLab 8.15+) + - the labels (GitLab 8.15+) +- Repository public access is retained. If a repository is private in Gitea + it will be created as private in GitLab as well. + +## How it works + +Since Gitea is currently not an OAuth provider, author/assignee cannot be mapped +to users in your GitLab's instance. This means that the project creator (most of +the times the current user that started the import process) is set as the author, +but a reference on the issue about the original Gitea author is kept. + +The importer will create any new namespaces (groups) if they don't exist or in +the case the namespace is taken, the repository will be imported under the user's +namespace that started the import process. + +## Importing your Gitea repositories + +The importer page is visible when you create a new project. + + + +Click on the **Gitea** link and the import authorization process will start. + + + +### Authorize access to your repositories using a personal access token + +With this method, you will perform a one-off authorization with Gitea to grant +GitLab access your repositories: + +1. Go to <https://you-gitea-instance/user/settings/applications> (replace + `you-gitea-instance` with the host of your Gitea instance). +1. Click **Generate New Token**. +1. Enter a token description. +1. Click **Generate Token**. +1. Copy the token hash. +1. Go back to GitLab and provide the token to the Gitea importer. +1. Hit the **List Your Gitea Repositories** button and wait while GitLab reads + your repositories' information. Once done, you'll be taken to the importer + page to select the repositories to import. + +### Select which repositories to import + +After you've authorized access to your Gitea repositories, you will be +redirected to the Gitea importer page. + +From there, you can see the import statuses of your Gitea repositories. + +- Those that are being imported will show a _started_ status, +- those already successfully imported will be green with a _done_ status, +- whereas those that are not yet imported will have an **Import** button on the + right side of the table. + +If you want, you can import all your Gitea projects in one go by hitting +**Import all projects** in the upper left corner. + + + +--- + +You can also choose a different name for the project and a different namespace, +if you have the privileges to do so. diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index c36dfdb78ecdcbd3996b7c2812a6f4f55c586dd3..86a016fc6d62abdfce52f095f8ef5a0721ac6e28 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -6,8 +6,9 @@ Import your projects from GitHub to GitLab with minimal effort. >**Note:** If you are an administrator you can enable the [GitHub integration][gh-import] -in your GitLab instance sitewide. This configuration is optional, users will be -able import their GitHub repositories with a [personal access token][gh-token]. +in your GitLab instance sitewide. This configuration is optional, users will +still be able to import their GitHub repositories with a +[personal access token][gh-token]. - At its current state, GitHub importer can import: - the repository description (GitLab 7.7+) @@ -40,7 +41,7 @@ namespace that started the import process. The importer page is visible when you create a new project. - + Click on the **GitHub** link and the import authorization process will start. There are two ways to authorize access to your GitHub repositories: @@ -85,7 +86,7 @@ authorization with GitHub to grant GitLab access your repositories: 1. Click **Generate token**. 1. Copy the token hash. 1. Go back to GitLab and provide the token to the GitHub importer. -1. Hit the **List your GitHub repositories** button and wait while GitLab reads +1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads your repositories' information. Once done, you'll be taken to the importer page to select the repositories to import. @@ -112,7 +113,6 @@ You can also choose a different name for the project and a different namespace, if you have the privileges to do so. [gh-import]: ../../integration/github.md "GitHub integration" -[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" [gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration [gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token [social sign-in]: ../../profile/account/social_sign_in.md diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index dcc00074b750e392459ad4eb24f44b2c00a1c534..b27125a44de8e4c902fbc2af761fbf7c7ebc6af6 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -5,6 +5,9 @@ GitLab support is enabled on your GitLab instance. You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. +>**Note:** +If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.md) +  Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md index 423b095e69e7558c2b42643e0fe1bd4ccc4e4ff3..7a3628a39d70a8723a5ea47a4c567e09ac577d3f 100644 --- a/doc/workflow/importing/migrating_from_svn.md +++ b/doc/workflow/importing/migrating_from_svn.md @@ -79,7 +79,7 @@ Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the initial translation of existing SVN revisions into the Git repository: ``` -subgit install $GIT_REPOS_PATH +subgit install $GIT_REPO_PATH ``` After the initial translation is completed, the Git repository and the SVN diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index b3c73e947f03510e47c8a82e29b388d3d5edfb1e..5f6a718135d898bd051f896881663bb75af6629d 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -40,6 +40,12 @@ In `config/gitlab.yml`: storage_path: /mnt/storage/lfs-objects ``` +## Storage statistics + +You can see the total storage used for LFS objects on groups and projects +in the administration area, as well as through the [groups](../api/groups.md) +and [projects APIs](../api/projects.md). + ## Known limitations * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) @@ -47,3 +53,5 @@ In `config/gitlab.yml`: * Currently, removing LFS objects from GitLab Git LFS storage is not supported * LFS authentications via SSH was added with GitLab 8.12 * Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2. +* The storage statistics currently count each LFS object multiple times for + every project linking to it diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 6a7098e79d08c8878c00e1b8ec4d6b836eb96159..8c5020bee37cd6b1c94eafb22c804294d1caa720 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -134,7 +134,6 @@ This behaviour is caused by Git LFS using HTTPS connections by default when a To prevent this from happening, set the lfs url in project Git config: ```bash - git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs" ``` diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index c936e8833c6e0770b377e465109235e41f685d10..4c52974e1038447b3de0f725b9e33b2c247fe765 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to: ...with notification level "Participating" or higher -- Watchers: users with notification level "Watch" +- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers) - Subscribers: anyone who manually subscribed to the issue/merge request - Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below diff --git a/doc/workflow/time-tracking/time-tracking-example.png b/doc/workflow/time-tracking/time-tracking-example.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcabb602d67655043daa1b55bd37674642ba91a Binary files /dev/null and b/doc/workflow/time-tracking/time-tracking-example.png differ diff --git a/doc/workflow/time-tracking/time-tracking-sidebar.png b/doc/workflow/time-tracking/time-tracking-sidebar.png new file mode 100644 index 0000000000000000000000000000000000000000..d1ff5571f95e7cd0d3bf284227727f3e2255e7ae Binary files /dev/null and b/doc/workflow/time-tracking/time-tracking-sidebar.png differ diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md new file mode 100644 index 0000000000000000000000000000000000000000..de12994c516e6bafa7e523341c38e8215beedb41 --- /dev/null +++ b/doc/workflow/time_tracking.md @@ -0,0 +1,73 @@ +# Time Tracking + +> Introduced in GitLab 8.14. + +Time Tracking allows you to track estimates and time spent on issues and merge +requests within GitLab. + +## Overview + +Time Tracking lets you: +* record the time spent working on an issue or a merge request, +* add an estimate of the amount of time needed to complete an issue or a merge +request. + +You don't have to indicate an estimate to enter the time spent, and vice versa. + +Data about time tracking is shown on the issue/merge request sidebar, as shown +below. + + + +## How to enter data + +Time Tracking uses two [slash commands] that GitLab introduced with this new +feature: `/spend` and `/estimate`. + +Slash commands can be used in the body of an issue or a merge request, but also +in a comment in both an issue or a merge request. + +Below is an example of how you can use those new slash commands inside a comment. + + + +Adding time entries (time spent or estimates) is limited to project members. + +### Estimates + +To enter an estimate, write `/estimate`, followed by the time. For example, if +you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write +`/estimate 3d 5h 10m`. + +Every time you enter a new time estimate, any previous time estimates will be +overridden by this new value. There should only be one valid estimate in an +issue or a merge request. + +To remove an estimation entirely, use `/remove_estimation`. + +### Time spent + +To enter a time spent, use `/spend 3d 5h 10m`. + +Every new time spent entry will be added to the current total time spent for the +issue or the merge request. + +You can remove time by entering a negative amount: `/spend -3d` will remove 3 +days from the total time spent. You can't go below 0 minutes of time spent, +so GitLab will automatically reset the time spent if you remove a larger amount +of time compared to the time that was entered already. + +To remove all the time spent at once, use `/remove_time_spent`. + +## Configuration + +The following time units are available: +* weeks (w) +* days (d) +* hours (h) +* minutes (m) + +Default conversion rates are 1w = 5d and 1d = 8h. + +[landing]: https://about.gitlab.com/features/time-tracking +[slash-commands]: ../user/project/slash_commands.md diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature deleted file mode 100644 index f5bb06dea7d76f8c9ca42edd220b6428fbdf1510..0000000000000000000000000000000000000000 --- a/features/admin/active_tab.feature +++ /dev/null @@ -1,54 +0,0 @@ -@admin -Feature: Admin Active Tab - Background: - Given I sign in as an admin - - Scenario: On Admin Home - Given I visit admin page - Then the active main tab should be Overview - And no other main tabs should be active - - Scenario: On Admin Projects - Given I visit admin projects page - Then the active main tab should be Overview - And the active sub tab should be Projects - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Groups - Given I visit admin groups page - Then the active main tab should be Overview - And the active sub tab should be Groups - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Users - Given I visit admin users page - Then the active main tab should be Overview - And the active sub tab should be Users - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Logs - Given I visit admin logs page - Then the active main tab should be Monitoring - And the active sub tab should be Logs - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Messages - Given I visit admin messages page - Then the active main tab should be Messages - And no other main tabs should be active - - Scenario: On Admin Hooks - Given I visit admin hooks page - Then the active main tab should be Hooks - And no other main tabs should be active - - Scenario: On Admin Resque - Given I visit admin Resque page - Then the active main tab should be Monitoring - And the active sub tab should be Resque - And no other main tabs should be active - And no other sub tabs should be active diff --git a/features/admin/appearance.feature b/features/admin/appearance.feature deleted file mode 100644 index 5c1dd7531c123f82b7d3468e47de3d180f5f462e..0000000000000000000000000000000000000000 --- a/features/admin/appearance.feature +++ /dev/null @@ -1,37 +0,0 @@ -Feature: Admin Appearance - Scenario: Create new appearance - Given I sign in as an admin - And I visit admin appearance page - When submit form with new appearance - Then I should be redirected to admin appearance page - And I should see newly created appearance - - Scenario: Preview appearance - Given application has custom appearance - And I sign in as an admin - When I visit admin appearance page - And I click preview button - Then I should see a customized appearance - - Scenario: Custom sign-in page - Given application has custom appearance - When I visit login page - Then I should see a customized appearance - - Scenario: Appearance logo - Given application has custom appearance - And I sign in as an admin - And I visit admin appearance page - When I attach a logo - Then I should see a logo - And I remove the logo - Then I should see logo removed - - Scenario: Header logos - Given application has custom appearance - And I sign in as an admin - And I visit admin appearance page - When I attach header logos - Then I should see header logos - And I remove the header logos - Then I should see header logos removed diff --git a/features/admin/applications.feature b/features/admin/applications.feature deleted file mode 100644 index 2a00e1666c01239e7c44dc1bd40e2a0344124955..0000000000000000000000000000000000000000 --- a/features/admin/applications.feature +++ /dev/null @@ -1,18 +0,0 @@ -@admin -Feature: Admin Applications - Background: - Given I sign in as an admin - And I visit applications page - - Scenario: I can manage application - Then I click on new application button - And I should see application form - Then I fill application form out and submit - And I see application - Then I click edit - And I see edit application form - Then I change name of application and submit - And I see that application was changed - Then I visit applications page - And I click to remove application - Then I see that application is removed \ No newline at end of file diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature deleted file mode 100644 index 4f9c651561e5018f222a30b6b3019bd2c70553c1..0000000000000000000000000000000000000000 --- a/features/admin/broadcast_messages.feature +++ /dev/null @@ -1,33 +0,0 @@ -@admin -Feature: Admin Broadcast Messages - Background: - Given I sign in as an admin - And application already has a broadcast message - And I visit admin messages page - - Scenario: See broadcast messages list - Then I should see all broadcast messages - - Scenario: Create a customized broadcast message - When submit form with new customized broadcast message - Then I should be redirected to admin messages page - And I should see newly created broadcast message - Then I visit dashboard page - And I should see a customized broadcast message - - Scenario: Edit an existing broadcast message - When I edit an existing broadcast message - And I change the broadcast message text - Then I should be redirected to admin messages page - And I should see the updated broadcast message - - Scenario: Remove an existing broadcast message - When I remove an existing broadcast message - Then I should be redirected to admin messages page - And I should not see the removed broadcast message - - @javascript - Scenario: Live preview a customized broadcast message - When I visit admin messages page - And I enter a broadcast message with Markdown - Then I should see a live preview of the rendered broadcast message diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature deleted file mode 100644 index 33439cd1e85d9678ef4b485a3ee400853867ca11..0000000000000000000000000000000000000000 --- a/features/admin/deploy_keys.feature +++ /dev/null @@ -1,16 +0,0 @@ -@admin -Feature: Admin Deploy Keys - Background: - Given I sign in as an admin - And there are public deploy keys in system - - Scenario: Deploy Keys list - When I visit admin deploy keys page - Then I should see all public deploy keys - - Scenario: Deploy Keys new - When I visit admin deploy keys page - And I click 'New Deploy Key' - And I submit new deploy key - Then I should be on admin deploy keys page - And I should see newly created deploy key diff --git a/features/admin/groups.feature b/features/admin/groups.feature deleted file mode 100644 index 657e847cf4ae3c4f3a9285614ca222cf5fe50f64..0000000000000000000000000000000000000000 --- a/features/admin/groups.feature +++ /dev/null @@ -1,49 +0,0 @@ -@admin -Feature: Admin Groups - Background: - Given I sign in as an admin - And I have group with projects - And User "John Doe" exists - And I visit admin groups page - - Scenario: See group list - Then I should be all groups - - Scenario: Create a group - When I click new group link - And submit form with new group info - Then I should be redirected to group page - And I should see newly created group - - @javascript - Scenario: Add user into projects in group - When I visit admin group page - When I select user "John Doe" from user list as "Reporter" - Then I should see "John Doe" in team list in every project as "Reporter" - - Scenario: Shared projects - Given group has shared projects - When I visit group page - Then I should see project shared with group - - @javascript - Scenario: Invite user to a group by e-mail - When I visit admin group page - When I select user "johndoe@gitlab.com" from user list as "Reporter" - Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter" - - @javascript - Scenario: Signed in admin should be able to add himself to a group - Given "John Doe" is owner of group "Owned" - When I visit group "Owned" members page - When I select current user as "Developer" - Then I should see current user as "Developer" - - @javascript - Scenario: Signed in admin should be able to remove himself from group - Given current user is developer of group "Owned" - When I visit group "Owned" members page - Then I should see current user as "Developer" - When I click on the "Remove User From Group" button for current user - When I visit group "Owned" members page - Then I should not see current user as "Developer" diff --git a/features/admin/labels.feature b/features/admin/labels.feature deleted file mode 100644 index 1af0e700bd4d3e4635dd4b0346bedf409ced32f3..0000000000000000000000000000000000000000 --- a/features/admin/labels.feature +++ /dev/null @@ -1,38 +0,0 @@ -Feature: Admin Issues Labels - Background: - Given I sign in as an admin - And I have labels: "bug", "feature", "enhancement" - Given I visit admin labels page - - Scenario: I should see labels list - Then I should see label 'bug' - And I should see label 'feature' - - Scenario: I create new label - Given I submit new label 'support' - Then I should see label 'support' - - Scenario: I edit label - Given I visit 'bug' label edit page - When I change label 'bug' to 'fix' - Then I should not see label 'bug' - Then I should see label 'fix' - - Scenario: I remove label - When I remove label 'bug' - Then I should not see label 'bug' - - @javascript - Scenario: I delete all labels - When I delete all labels - Then I should see labels help message - - Scenario: I create a label with invalid color - Given I visit admin new label page - When I submit new label with invalid color - Then I should see label color error message - - Scenario: I create a label that already exists - Given I visit admin new label page - When I submit new label 'bug' - Then I should see label exist error message diff --git a/features/admin/projects.feature b/features/admin/projects.feature deleted file mode 100644 index 8929bcf8d80b794b51e6a1ed62658fc2ae5fe143..0000000000000000000000000000000000000000 --- a/features/admin/projects.feature +++ /dev/null @@ -1,47 +0,0 @@ -@admin -Feature: Admin Projects - Background: - Given I sign in as an admin - And there are projects in system - - Scenario: I should see non-archived projects in the list - Given archived project "Archive" - When I visit admin projects page - Then I should see all non-archived projects - And I should not see project "Archive" - - @javascript - Scenario: I should see all projects in the list - Given archived project "Archive" - When I visit admin projects page - And I select "Show archived projects" - Then I should see all projects - And I should see "archived" label - - Scenario: Projects show - When I visit admin projects page - And I click on first project - Then I should see project details - - @javascript - Scenario: Transfer project - Given group 'Web' - And I visit admin project page - When I transfer project to group 'Web' - Then I should see project transfered - - @javascript - Scenario: Signed in admin should be able to add himself to a project - Given "John Doe" owns private project "Enterprise" - When I visit project "Enterprise" members page - When I select current user as "Developer" - Then I should see current user as "Developer" - - @javascript - Scenario: Signed in admin should be able to remove himself from a project - Given "John Doe" owns private project "Enterprise" - And current user is developer of project "Enterprise" - When I visit project "Enterprise" members page - Then I should see current user as "Developer" - When I click on the "Remove User From Project" button for current user - Then I should not see current user as "Developer" diff --git a/features/admin/users.feature b/features/admin/users.feature deleted file mode 100644 index 6755645778a1be679c78807be211d39acd442ce5..0000000000000000000000000000000000000000 --- a/features/admin/users.feature +++ /dev/null @@ -1,65 +0,0 @@ -@admin -Feature: Admin Users - Background: - Given I sign in as an admin - And system has users - - Scenario: On Admin Users - Given I visit admin users page - Then I should see all users - - Scenario: Edit user and change username to non ascii char - When I visit admin users page - And Click edit - And Input non ascii char in username - And Click save - Then See username error message - And Not changed form action url - - Scenario: Show user attributes - Given user "Mike" with groups and projects - Given I visit admin users page - And click on "Mike" link - Then I should see user "Mike" details - - Scenario: Edit my user attributes - Given I visit admin users page - And click edit on my user - When I submit modified user - Then I see user attributes changed - - @javascript - Scenario: Remove users secondary email - Given I visit admin users page - And I view the user with secondary email - And I see the secondary email - When I click remove secondary email - Then I should not see secondary email anymore - - Scenario: Show user keys - Given user "Pete" with ssh keys - And I visit admin users page - And click on user "Pete" - And click on ssh keys tab - Then I should see key list - And I click on the key title - Then I should see key details - And I click on remove key - Then I should see the key removed - - Scenario: Show user identities - Given user "Pete" with twitter account - And I visit "Pete" identities page in admin - Then I should see twitter details - - Scenario: Update user identities - Given user "Pete" with twitter account - And I visit "Pete" identities page in admin - And I modify twitter identity - Then I should see twitter details updated - - Scenario: Remove user identities - Given user "Pete" with twitter account - And I visit "Pete" identities page in admin - And I remove twitter identity - Then I should not see twitter details diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature deleted file mode 100644 index bd883a0ebfafb09e6838fd4968cbb26eba43f34d..0000000000000000000000000000000000000000 --- a/features/dashboard/active_tab.feature +++ /dev/null @@ -1,24 +0,0 @@ -@dashboard -Feature: Dashboard Active Tab - Background: - Given I sign in as a user - - Scenario: On Dashboard Home - Given I visit dashboard page - Then the active main tab should be Home - And no other main tabs should be active - - Scenario: On Dashboard Issues - Given I visit dashboard issues page - Then the active main tab should be Issues - And no other main tabs should be active - - Scenario: On Dashboard Merge Requests - Given I visit dashboard merge requests page - Then the active main tab should be Merge Requests - And no other main tabs should be active - - Scenario: On Dashboard Groups - Given I visit dashboard groups page - Then the active main tab should be Groups - And no other main tabs should be active diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature deleted file mode 100644 index bed9282f1c658208006a31b93e337d6b67ea4b56..0000000000000000000000000000000000000000 --- a/features/dashboard/archived_projects.feature +++ /dev/null @@ -1,17 +0,0 @@ -@dashboard -Feature: Dashboard Archived Projects - Background: - Given I sign in as a user - And I own project "Shop" - And I own project "Forum" - And project "Forum" is archived - And I visit dashboard page - - Scenario: I should see non-archived projects on dashboard - Then I should see "Shop" project link - And I should not see "Forum" project link - - Scenario: I toggle show of archived projects on dashboard - When I click "Show archived projects" link - Then I should see "Shop" project link - And I should see "Forum" project link diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature deleted file mode 100644 index 3ae2c679dc1a8aa7dd7842854c89c239ccce797d..0000000000000000000000000000000000000000 --- a/features/dashboard/group.feature +++ /dev/null @@ -1,13 +0,0 @@ -@dashboard -Feature: Dashboard Group - Background: - Given I sign in as "John Doe" - And "John Doe" is owner of group "Owned" - And "John Doe" is guest of group "Guest" - - Scenario: Create a group from dasboard - And I visit dashboard groups page - And I click new group link - And submit form with new group "Samurai" info - Then I should be redirected to group "Samurai" page - And I should see newly created group "Samurai" diff --git a/features/dashboard/help.feature b/features/dashboard/help.feature deleted file mode 100644 index bca2772897b0a78722834360da8323ecc978777d..0000000000000000000000000000000000000000 --- a/features/dashboard/help.feature +++ /dev/null @@ -1,9 +0,0 @@ -@dashboard -Feature: Dashboard Help - Background: - Given I sign in as a user - And I visit the "Rake Tasks" help page - - Scenario: The markdown should be rendered correctly - Then I should see "Rake Tasks" page markdown rendered - And Header "Rebuild project satellites" should have correct ids and links diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 1776c07e60e1d32245bd9e9a7ea368a563a40cfc..3459cce03f9edb33046343e0dbee3645fd4e67f7 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -47,8 +47,6 @@ Feature: Project Commits And repository contains ".gitlab-ci.yml" file When I click on commit link Then I see commit ci info - And I click status link - Then I see builds list Scenario: I browse commit with side-by-side diff view Given I click on commit link diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature deleted file mode 100644 index 49d7a3b9af248d1888539d7d6fa416f1457f0d71..0000000000000000000000000000000000000000 --- a/features/project/issues/filter_labels.feature +++ /dev/null @@ -1,28 +0,0 @@ -@project_issues -Feature: Project Issues Filter Labels - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - And project "Shop" has issue "Bugfix1" with labels: "bug", "feature" - And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement" - And project "Shop" has issue "Feature1" with labels: "feature" - Given I visit project "Shop" issues page - - @javascript - Scenario: I filter by one label - Given I click link "bug" - And I click "dropdown close button" - Then I should see "Bugfix1" in issues list - And I should see "Bugfix2" in issues list - And I should not see "Feature1" in issues list - - # TODO: make labels filter works according to this scanario - # right now it looks for label 1 OR label 2. Old behaviour (this test) was - # all issues that have both label 1 AND label 2 - #Scenario: I filter by two labels - #Given I click link "bug" - #And I click link "feature" - #Then I should see "Bugfix1" in issues list - #And I should not see "Bugfix2" in issues list - #And I should not see "Feature1" in issues list diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 80670063ea00e0b89269d17ce88b3f35f098967b..b2b4fe722205eccb7a5e4431c74f7c693baeefce 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -26,12 +26,6 @@ Feature: Project Issues Given I click link "Release 0.4" Then I should see issue "Release 0.4" - @javascript - Scenario: I filter by author - Given I add a user to project "Shop" - And I click "author" dropdown - Then I see current user as the first user - Scenario: I submit new unassigned issue Given I click link "New Issue" And I submit new issue "500 error on profile" @@ -84,56 +78,6 @@ Feature: Project Issues And I sort the list by "Least popular" Then The list should be sorted by "Least popular" - @javascript - Scenario: I search issue - Given I fill in issue search with "Re" - Then I should see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: I search issue that not exist - Given I fill in issue search with "Bu" - Then I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - - @javascript - Scenario: I search all issues - Given I click link "All" - And I fill in issue search with ".3" - Then I should see "Release 0.3" in issues - And I should not see "Release 0.4" in issues - - @javascript - Scenario: Search issues when search string exactly matches issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And I fill in issue search with 'Description for issue1' - Then I should see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: Search issues when search string partially matches issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And project 'Shop' has issue 'Feature1' with description: 'Feature submitted for issue1' - And I fill in issue search with 'issue1' - Then I should see 'Feature1' in issues - Then I should see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: Search issues when search string matches no issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And I fill in issue search with 'Rock and roll' - Then I should not see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - # Markdown Scenario: Headers inside the description should have ids generated for them. diff --git a/features/project/service.feature b/features/project/service.feature index 3a7b830852489d2560619a4aabc6203023827cff..cce5f58adec70ba5fb496b22e0a8cb8cedf2f2fa 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -37,11 +37,11 @@ Feature: Project Services And I fill Assembla settings Then I should see Assembla service settings saved - Scenario: Activate Slack service + Scenario: Activate Slack notifications service When I visit project "Shop" services page - And I click Slack service link - And I fill Slack settings - Then I should see Slack service settings saved + And I click Slack notifications service link + And I fill Slack notifications settings + Then I should see Slack Notifications service settings saved Scenario: Activate Pushover service When I visit project "Shop" services page diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb deleted file mode 100644 index 9b1689a8198212d1a00e40495f74d7b06e40ba58..0000000000000000000000000000000000000000 --- a/features/steps/admin/active_tab.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Overview' do - ensure_active_main_tab('Overview') - end - - step 'the active sub tab should be Projects' do - ensure_active_sub_tab('Projects') - end - - step 'the active sub tab should be Groups' do - ensure_active_sub_tab('Groups') - end - - step 'the active sub tab should be Users' do - ensure_active_sub_tab('Users') - end - - step 'the active main tab should be Hooks' do - ensure_active_main_tab('Hooks') - end - - step 'the active main tab should be Monitoring' do - ensure_active_main_tab('Monitoring') - end - - step 'the active sub tab should be Resque' do - ensure_active_sub_tab('Background Jobs') - end - - step 'the active sub tab should be Logs' do - ensure_active_sub_tab('Logs') - end - - step 'the active main tab should be Messages' do - ensure_active_main_tab('Messages') - end -end diff --git a/features/steps/admin/appearance.rb b/features/steps/admin/appearance.rb deleted file mode 100644 index 0d1be46d11dfcb630b5fd31f74a3866d6201772f..0000000000000000000000000000000000000000 --- a/features/steps/admin/appearance.rb +++ /dev/null @@ -1,72 +0,0 @@ -class Spinach::Features::AdminAppearance < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - - step 'submit form with new appearance' do - fill_in 'appearance_title', with: 'MyCompany' - fill_in 'appearance_description', with: 'dev server' - click_button 'Save' - end - - step 'I should be redirected to admin appearance page' do - expect(current_path).to eq admin_appearances_path - expect(page).to have_content 'Appearance settings' - end - - step 'I should see newly created appearance' do - expect(page).to have_field('appearance_title', with: 'MyCompany') - expect(page).to have_field('appearance_description', with: 'dev server') - expect(page).to have_content 'Last edit' - end - - step 'I click preview button' do - click_link "Preview" - end - - step 'application has custom appearance' do - create(:appearance) - end - - step 'I should see a customized appearance' do - expect(page).to have_content appearance.title - expect(page).to have_content appearance.description - end - - step 'I attach a logo' do - attach_file(:appearance_logo, Rails.root.join('spec', 'fixtures', 'dk.png')) - click_button 'Save' - end - - step 'I attach header logos' do - attach_file(:appearance_header_logo, Rails.root.join('spec', 'fixtures', 'dk.png')) - click_button 'Save' - end - - step 'I should see a logo' do - expect(page).to have_xpath('//img[@src="/uploads/appearance/logo/1/dk.png"]') - end - - step 'I should see header logos' do - expect(page).to have_xpath('//img[@src="/uploads/appearance/header_logo/1/dk.png"]') - end - - step 'I remove the logo' do - click_link 'Remove logo' - end - - step 'I remove the header logos' do - click_link 'Remove header logo' - end - - step 'I should see logo removed' do - expect(page).not_to have_xpath('//img[@src="/uploads/appearance/logo/1/gitlab_logo.png"]') - end - - step 'I should see header logos removed' do - expect(page).not_to have_xpath('//img[@src="/uploads/appearance/header_logo/1/header_logo_light.png"]') - end - - def appearance - Appearance.last - end -end diff --git a/features/steps/admin/applications.rb b/features/steps/admin/applications.rb deleted file mode 100644 index 7c12cb969211bac8776bc3bae7c88e936cd0b356..0000000000000000000000000000000000000000 --- a/features/steps/admin/applications.rb +++ /dev/null @@ -1,55 +0,0 @@ -class Spinach::Features::AdminApplications < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - step 'I click on new application button' do - click_on 'New Application' - end - - step 'I should see application form' do - expect(page).to have_content "New application" - end - - step 'I fill application form out and submit' do - fill_in :doorkeeper_application_name, with: 'test' - fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' - click_on "Submit" - end - - step 'I see application' do - expect(page).to have_content "Application: test" - expect(page).to have_content "Application Id" - expect(page).to have_content "Secret" - end - - step 'I click edit' do - click_on "Edit" - end - - step 'I see edit application form' do - expect(page).to have_content "Edit application" - end - - step 'I change name of application and submit' do - expect(page).to have_content "Edit application" - fill_in :doorkeeper_application_name, with: 'test_changed' - click_on "Submit" - end - - step 'I see that application was changed' do - expect(page).to have_content "test_changed" - expect(page).to have_content "Application Id" - expect(page).to have_content "Secret" - end - - step 'I click to remove application' do - page.within '.oauth-applications' do - click_on "Destroy" - end - end - - step "I see that application is removed" do - expect(page.find(".oauth-applications")).not_to have_content "test_changed" - end -end diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb deleted file mode 100644 index af2b4a2931384f5ea0831faa232fc0c696843cf2..0000000000000000000000000000000000000000 --- a/features/steps/admin/broadcast_messages.rb +++ /dev/null @@ -1,66 +0,0 @@ -class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - - step 'application already has a broadcast message' do - FactoryGirl.create(:broadcast_message, :expired, message: "Migration to new server") - end - - step 'I should see all broadcast messages' do - expect(page).to have_content "Migration to new server" - end - - step 'I should be redirected to admin messages page' do - expect(current_path).to eq admin_broadcast_messages_path - end - - step 'I should see newly created broadcast message' do - expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' - end - - step 'submit form with new customized broadcast message' do - fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**' - fill_in 'broadcast_message_color', with: '#f2dede' - fill_in 'broadcast_message_font', with: '#b94a48' - select Date.today.next_year.year, from: "broadcast_message_ends_at_1i" - click_button "Add broadcast message" - end - - step 'I should see a customized broadcast message' do - expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' - expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST' - expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) - end - - step 'I edit an existing broadcast message' do - click_link 'Edit' - end - - step 'I change the broadcast message text' do - fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW' - click_button 'Update broadcast message' - end - - step 'I should see the updated broadcast message' do - expect(page).to have_content "Application update RIGHT NOW" - end - - step 'I remove an existing broadcast message' do - click_link 'Remove' - end - - step 'I should not see the removed broadcast message' do - expect(page).not_to have_content 'Migration to new server' - end - - step 'I enter a broadcast message with Markdown' do - fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:" - end - - step 'I should see a live preview of the rendered broadcast message' do - page.within('.broadcast-message-preview') do - expect(page).to have_selector('strong', text: 'Markdown') - expect(page).to have_selector('img.emoji') - end - end -end diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb deleted file mode 100644 index 56787eeb6b3b92d7172003859930665fd90a81f0..0000000000000000000000000000000000000000 --- a/features/steps/admin/deploy_keys.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - step 'there are public deploy keys in system' do - create(:deploy_key, public: true) - create(:another_deploy_key, public: true) - end - - step 'I should see all public deploy keys' do - DeployKey.are_public.each do |p| - expect(page).to have_content p.title - end - end - - step 'I visit admin deploy key page' do - visit admin_deploy_key_path(deploy_key) - end - - step 'I visit admin deploy keys page' do - visit admin_deploy_keys_path - end - - step 'I click \'New Deploy Key\'' do - click_link 'New Deploy Key' - end - - step 'I submit new deploy key' do - fill_in "deploy_key_title", with: "laptop" - fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" - click_button "Create" - end - - step 'I should be on admin deploy keys page' do - expect(current_path).to eq admin_deploy_keys_path - end - - step 'I should see newly created deploy key' do - expect(page).to have_content(deploy_key.title) - end - - def deploy_key - @deploy_key ||= DeployKey.are_public.first - end -end diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb deleted file mode 100644 index 9396a76f0a288c7857433dc555f03b58562ac3cd..0000000000000000000000000000000000000000 --- a/features/steps/admin/groups.rb +++ /dev/null @@ -1,143 +0,0 @@ -class Spinach::Features::AdminGroups < Spinach::FeatureSteps - include SharedAuthentication - include SharedGroup - include SharedPaths - include SharedUser - include SharedActiveTab - include Select2Helper - - When 'I visit admin group page' do - visit admin_group_path(current_group) - end - - When 'I click new group link' do - click_link "New Group" - end - - step 'I have group with projects' do - @group = create(:group) - @project = create(:project, group: @group) - @event = create(:closed_issue_event, project: @project) - - @project.team << [current_user, :master] - end - - step 'submit form with new group info' do - fill_in 'group_path', with: 'gitlab' - fill_in 'group_description', with: 'Group description' - click_button "Create group" - end - - step 'I should see newly created group' do - expect(page).to have_content "Group: gitlab" - expect(page).to have_content "Group description" - end - - step 'I should be redirected to group page' do - expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab')) - end - - When 'I select user "John Doe" from user list as "Reporter"' do - select2(user_john.id, from: "#user_ids", multiple: true) - page.within "#new_project_member" do - select "Reporter", from: "access_level" - end - click_button "Add users to group" - end - - When 'I select user "johndoe@gitlab.com" from user list as "Reporter"' do - select2('johndoe@gitlab.com', from: "#user_ids", multiple: true) - page.within "#new_project_member" do - select "Reporter", from: "access_level" - end - click_button "Add users to group" - end - - step 'I should see "John Doe" in team list in every project as "Reporter"' do - page.within ".group-users-list" do - expect(page).to have_content "John Doe" - expect(page).to have_content "Reporter" - end - end - - step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do - page.within ".group-users-list" do - expect(page).to have_content "johndoe@gitlab.com" - expect(page).to have_content "Invited by" - expect(page).to have_content "Reporter" - end - end - - step 'I should be all groups' do - Group.all.each do |group| - expect(page).to have_content group.name - end - end - - step 'group has shared projects' do - share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER) - share_link.group_id = current_group.id - share_link.save! - end - - step 'I visit group page' do - visit admin_group_path(current_group) - end - - step 'I should see project shared with group' do - expect(page).to have_content(shared_project.name_with_namespace) - expect(page).to have_content "Projects shared with" - end - - step 'we have user "John Doe" in group' do - current_group.add_reporter(user_john) - end - - step 'I should not see "John Doe" in team list' do - page.within ".group-users-list" do - expect(page).not_to have_content "John Doe" - end - end - - step 'I select current user as "Developer"' do - page.within ".users-group-form" do - select2(current_user.id, from: "#user_ids", multiple: true) - select "Developer", from: "access_level" - end - - click_button "Add to group" - end - - step 'I should see current user as "Developer"' do - page.within '.content-list' do - expect(page).to have_content(current_user.name) - expect(page).to have_content('Developer') - end - end - - step 'I click on the "Remove User From Group" button for current user' do - find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I should not see current user as "Developer"' do - page.within '.content-list' do - expect(page).not_to have_content(current_user.name) - expect(page).not_to have_content('Developer') - end - end - - protected - - def current_group - @group ||= Group.first - end - - def shared_project - @shared_project ||= create(:empty_project) - end - - def user_john - @user_john ||= User.find_by(name: "John Doe") - end -end diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb deleted file mode 100644 index 55ddcc25085f3046d860ddf4587f8dc173b8664b..0000000000000000000000000000000000000000 --- a/features/steps/admin/labels.rb +++ /dev/null @@ -1,117 +0,0 @@ -class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I visit \'bug\' label edit page' do - visit edit_admin_label_path(bug_label) - end - - step 'I visit admin new label page' do - visit new_admin_label_path - end - - step 'I visit admin labels page' do - visit admin_labels_path - end - - step 'I remove label \'bug\'' do - page.within "#label_#{bug_label.id}" do - click_link 'Delete' - end - end - - step 'I have labels: "bug", "feature", "enhancement"' do - ["bug", "feature", "enhancement"].each do |title| - Label.create(title: title, template: true) - end - end - - step 'I delete all labels' do - page.within '.labels' do - page.all('.btn-remove').each do |remove| - remove.click - sleep 0.05 - end - end - end - - step 'I should see labels help message' do - page.within '.labels' do - expect(page).to have_content 'There are no labels yet' - end - end - - step 'I submit new label \'support\'' do - visit new_admin_label_path - fill_in 'Title', with: 'support' - fill_in 'Background color', with: '#F95610' - click_button 'Save' - end - - step 'I submit new label \'bug\'' do - visit new_admin_label_path - fill_in 'Title', with: 'bug' - fill_in 'Background color', with: '#F95610' - click_button 'Save' - end - - step 'I submit new label with invalid color' do - visit new_admin_label_path - fill_in 'Title', with: 'support' - fill_in 'Background color', with: '#12' - click_button 'Save' - end - - step 'I should see label exist error message' do - page.within '.label-form' do - expect(page).to have_content 'Title has already been taken' - end - end - - step 'I should see label color error message' do - page.within '.label-form' do - expect(page).to have_content 'Color must be a valid color code' - end - end - - step 'I should see label \'feature\'' do - page.within '.manage-labels-list' do - expect(page).to have_content 'feature' - end - end - - step 'I should see label \'bug\'' do - page.within '.manage-labels-list' do - expect(page).to have_content 'bug' - end - end - - step 'I should not see label \'bug\'' do - page.within '.manage-labels-list' do - expect(page).not_to have_content 'bug' - end - end - - step 'I should see label \'support\'' do - page.within '.manage-labels-list' do - expect(page).to have_content 'support' - end - end - - step 'I change label \'bug\' to \'fix\'' do - fill_in 'Title', with: 'fix' - fill_in 'Background color', with: '#F15610' - click_button 'Save' - end - - step 'I should see label \'fix\'' do - page.within '.manage-labels-list' do - expect(page).to have_content 'fix' - end - end - - def bug_label - Label.templates.find_or_create_by(title: 'bug') - end -end diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb deleted file mode 100644 index 2b8cd030acef899b1a5e4a2ad556c2516e5cd8cd..0000000000000000000000000000000000000000 --- a/features/steps/admin/projects.rb +++ /dev/null @@ -1,104 +0,0 @@ -class Spinach::Features::AdminProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - include SharedProject - include SharedUser - include Select2Helper - - step 'I should see all non-archived projects' do - Project.non_archived.each do |p| - expect(page).to have_content p.name_with_namespace - end - end - - step 'I should see all projects' do - Project.all.each do |p| - expect(page).to have_content p.name_with_namespace - end - end - - step 'I select "Show archived projects"' do - find(:css, '#sort-projects-dropdown').click - click_link 'Show archived projects' - end - - step 'I should see "archived" label' do - expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') - end - - step 'I click on first project' do - click_link Project.first.name_with_namespace - end - - step 'I should see project details' do - project = Project.first - expect(current_path).to eq admin_namespace_project_path(project.namespace, project) - expect(page).to have_content(project.name_with_namespace) - expect(page).to have_content(project.creator.name) - end - - step 'I visit admin project page' do - visit admin_namespace_project_path(project.namespace, project) - end - - step 'I transfer project to group \'Web\'' do - allow_any_instance_of(Projects::TransferService). - to receive(:move_uploads_to_new_namespace).and_return(true) - click_button 'Search for Namespace' - click_link 'group: web' - click_button 'Transfer' - end - - step 'group \'Web\'' do - create(:group, name: 'Web') - end - - step 'I should see project transfered' do - expect(page).to have_content 'Web / ' + project.name - expect(page).to have_content 'Namespace: Web' - end - - step 'I visit project "Enterprise" members page' do - project = Project.find_by!(name: "Enterprise") - visit namespace_project_project_members_path(project.namespace, project) - end - - step 'I select current user as "Developer"' do - page.within ".users-project-form" do - select2(current_user.id, from: "#user_ids", multiple: true) - select "Developer", from: "access_level" - end - - click_button "Add to project" - end - - step 'I should see current user as "Developer"' do - page.within '.content-list' do - expect(page).to have_content(current_user.name) - expect(page).to have_content('Developer') - end - end - - step 'current user is developer of project "Enterprise"' do - project = Project.find_by!(name: "Enterprise") - project.team << [current_user, :developer] - end - - step 'I click on the "Remove User From Project" button for current user' do - find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I should not see current_user as "Developer"' do - expect(page).not_to have_selector(:css, '.content-list') - end - - def project - @project ||= Project.first - end - - def group - Group.find_by(name: 'Web') - end -end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb deleted file mode 100644 index 8fb8a86d58b1f84dab98990f4d2a001b8f973212..0000000000000000000000000000000000000000 --- a/features/steps/admin/users.rb +++ /dev/null @@ -1,167 +0,0 @@ -class Spinach::Features::AdminUsers < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - before do - allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) - allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path) - end - - after do - allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original - allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original - end - - step 'I should see all users' do - User.all.each do |user| - expect(page).to have_content user.name - end - end - - step 'Click edit' do - @user = User.first - find("#edit_user_#{@user.id}").click - end - - step 'Input non ascii char in username' do - fill_in 'user_username', with: "\u3042\u3044" - end - - step 'Click save' do - click_button("Save") - end - - step 'See username error message' do - page.within "#error_explanation" do - expect(page).to have_content "Username" - end - end - - step 'Not changed form action url' do - expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"]) - end - - step 'I submit modified user' do - check :user_can_create_group - click_button 'Save' - end - - step 'I see user attributes changed' do - expect(page).to have_content 'Can create groups: Yes' - end - - step 'click edit on my user' do - find("#edit_user_#{current_user.id}").click - end - - step 'I view the user with secondary email' do - @user_with_secondary_email = User.last - @user_with_secondary_email.emails.new(email: "secondary@example.com") - @user_with_secondary_email.save - visit "/admin/users/#{@user_with_secondary_email.username}" - end - - step 'I see the secondary email' do - expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}" - end - - step 'I click remove secondary email' do - find("#remove_email_#{@user_with_secondary_email.emails.last.id}").click - end - - step 'I should not see secondary email anymore' do - expect(page).not_to have_content "Secondary email:" - end - - step 'user "Mike" with groups and projects' do - user = create(:user, name: 'Mike') - - project = create(:empty_project) - project.team << [user, :developer] - - group = create(:group) - group.add_developer(user) - end - - step 'click on "Mike" link' do - click_link "Mike" - end - - step 'I should see user "Mike" details' do - expect(page).to have_content 'Account' - expect(page).to have_content 'Personal projects limit' - end - - step 'user "Pete" with ssh keys' do - user = create(:user, name: 'Pete') - create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1") - create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2") - end - - step 'click on user "Pete"' do - click_link 'Pete' - end - - step 'I should see key list' do - expect(page).to have_content 'ssh-rsa Key2' - expect(page).to have_content 'ssh-rsa Key1' - end - - step 'I click on the key title' do - click_link 'ssh-rsa Key2' - end - - step 'I should see key details' do - expect(page).to have_content 'ssh-rsa Key2' - expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2' - end - - step 'I click on remove key' do - click_link 'Remove' - end - - step 'I should see the key removed' do - expect(page).not_to have_content 'ssh-rsa Key2' - end - - step 'user "Pete" with twitter account' do - @user = create(:user, name: 'Pete') - @user.identities.create!(extern_uid: '123456', provider: 'twitter') - end - - step 'I visit "Pete" identities page in admin' do - visit admin_user_identities_path(@user) - end - - step 'I should see twitter details' do - expect(page).to have_content 'Pete' - expect(page).to have_content 'twitter' - end - - step 'I modify twitter identity' do - find('.table').find(:link, 'Edit').click - fill_in 'identity_extern_uid', with: '654321' - select 'twitter_updated', from: 'identity_provider' - click_button 'Save changes' - end - - step 'I should see twitter details updated' do - expect(page).to have_content 'Pete' - expect(page).to have_content 'twitter_updated' - expect(page).to have_content '654321' - end - - step 'I remove twitter identity' do - click_link 'Delete' - end - - step 'I should not see twitter details' do - expect(page).to have_content 'Pete' - expect(page).not_to have_content 'twitter' - end - - step 'click on ssh keys tab' do - click_link 'SSH keys' - end -end diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb deleted file mode 100644 index 04fe96cef22bc61a6ee3a2d70cc48a3ded03b43c..0000000000000000000000000000000000000000 --- a/features/steps/dashboard/active_tab.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedSidebarActiveTab -end diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb deleted file mode 100644 index 6510f8d9b3267bbc2fd1103b333826a13741532e..0000000000000000000000000000000000000000 --- a/features/steps/dashboard/archived_projects.rb +++ /dev/null @@ -1,26 +0,0 @@ -class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - When 'project "Forum" is archived' do - project = Project.find_by(name: "Forum") - project.update_attribute(:archived, true) - end - - step 'I should see "Shop" project link' do - expect(page).to have_link "Shop" - end - - step 'I should not see "Forum" project link' do - expect(page).not_to have_link "Forum" - end - - step 'I should see "Forum" project link' do - expect(page).to have_link "Forum" - end - - step 'I click "Show archived projects" link' do - click_link "Show archived projects" - end -end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index b2bec369e0f8e29d90581b4442be88ff061db666..33a1c88e33c126e35a4f61efe5a0dff55f704274 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -35,7 +35,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I have group with projects' do @group = create(:group) - @project = create(:project, namespace: @group) + @project = create(:empty_project, namespace: @group) @event = create(:closed_issue_event, project: @project) @project.team << [current_user, :master] @@ -54,8 +54,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'group has a projects that does not belongs to me' do - @forbidden_project1 = create(:project, group: @group) - @forbidden_project2 = create(:project, group: @group) + @forbidden_project1 = create(:empty_project, group: @group) + @forbidden_project2 = create(:empty_project, group: @group) end step 'I should see 1 project at group list' do diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb deleted file mode 100644 index cf679fea5308f7c9104f82e10850e59895870cf8..0000000000000000000000000000000000000000 --- a/features/steps/dashboard/group.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Spinach::Features::DashboardGroup < Spinach::FeatureSteps - include SharedAuthentication - include SharedGroup - include SharedPaths - include SharedUser - - step 'I click new group link' do - click_link "New Group" - end - - step 'submit form with new group "Samurai" info' do - fill_in 'group_path', with: 'Samurai' - fill_in 'group_description', with: 'Tokugawa Shogunate' - click_button "Create group" - end - - step 'I should be redirected to group "Samurai" page' do - expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) - end - - step 'I should see newly created group "Samurai"' do - expect(page).to have_content "Samurai" - expect(page).to have_content "Tokugawa Shogunate" - end -end diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb deleted file mode 100644 index 3c5bf44c538bb722bd64335f790412b7702f9ba3..0000000000000000000000000000000000000000 --- a/features/steps/dashboard/help.rb +++ /dev/null @@ -1,21 +0,0 @@ -class Spinach::Features::DashboardHelp < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedMarkdown - - step 'I visit the help page' do - visit help_path - end - - step 'I visit the "Rake Tasks" help page' do - visit help_page_path("administration/raketasks/maintenance") - end - - step 'I should see "Rake Tasks" page markdown rendered' do - expect(page).to have_content "Gather information about GitLab and the system it runs on" - end - - step 'Header "Rebuild project satellites" should have correct ids and links' do - header_should_have_correct_id_and_link(2, 'Check GitLab configuration', 'check-gitlab-configuration', '.documentation') - end -end diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index 39c65bb6cde36b45fec07e892574580551aa824f..4e15d79ae742ae5221a3cdd6f3f4b27d104cb746 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -79,13 +79,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps def project @project ||= begin - project = create :project + project = create(:empty_project) project.team << [current_user, :master] project end end def public_project - @public_project ||= create :project, :public + @public_project ||= create(:empty_project, :public) end end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 6777101fb155dc05097da72a31bd42983c81662c..909ffec3646fad1c7957c22e4be874f48136b919 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -105,14 +105,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps def project @project ||= begin - project = create :project + project = create(:project, :repository) project.team << [current_user, :master] project end end def public_project - @public_project ||= create :project, :public + @public_project ||= create(:project, :public, :repository) end def forked_project diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index f5fddab357df5c64d1d8db5f4a88bd33f59e7368..70e23098dde08e68c525e117ac2fd4fe7ee1e673 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -104,7 +104,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps group = owned_group %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| - project = create :project, path: path, group: group + project = create(:empty_project, path: path, group: group) milestone = create :milestone, title: "Version 7.2", project: project create(:label, project: project, title: 'bug') @@ -131,5 +131,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps issue.labels << project.labels.find_by(title: 'bug') issue.labels << project.labels.find_by(title: 'feature') end + + current_user.refresh_authorized_projects end end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 0c88838767cdc33d8ac8f6f0795264fad3c566ea..4dc87dc4d9c2915defb81b37849995ffc3196f15 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -109,7 +109,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'Group "Owned" has archived project' do group = Group.find_by(name: 'Owned') - @archived_project = create(:project, namespace: group, archived: true, path: "archived-project") + @archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project") end step 'I should see "archived" label' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index ea480d2ad68d7fe8299b53119278e0aa40b16e2a..24cfbaad7fe58f75bf5bee5a4322d61675f8ce15 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -162,7 +162,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I have group with projects' do @group = create(:group) @group.add_owner(current_user) - @project = create(:project, namespace: @group) + @project = create(:project, :repository, namespace: @group) @event = create(:closed_issue_event, project: @project) @project.team << [current_user, :master] diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 007dfb67a77641cede3fd6280cb58b9c7b0d4e36..18e267294e408939dd2f037520f67e3ad75c88b5 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Pipeline #1 for 570e7b2a pending" end - step 'I click status link' do - find('.commit-ci-menu').click_link "Builds" - end - - step 'I see builds list' do - expect(page).to have_content "Pipeline #1 for 570e7b2a pending" - expect(page).to have_content "1 build" - end - step 'I search "submodules" commits' do fill_in 'commits-search', with: 'submodules' end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 83b9ef48392d3c17f93f125b375d0b1c4a35cf9d..edf78f62f9a213f21b10ce8abb7243150ea1c6b9 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -46,11 +46,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'other projects have deploy keys' do - @second_project = create(:project, namespace: create(:group)) + @second_project = create(:empty_project, namespace: create(:group)) @second_project.team << [current_user, :master] create(:deploy_keys_project, project: @second_project) - @third_project = create(:project, namespace: create(:group)) + @third_project = create(:empty_project, namespace: create(:group)) @third_project.team << [current_user, :master] create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first) end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 70dbd030003d61677026e0cc2ba2dd8c18f091d9..9a6c04fba7ad4740ee718b1d42686df861da4d13 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -9,7 +9,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I am a member of project "Shop"' do - @project = create(:project, name: "Shop") + @project = create(:project, :repository, name: "Shop") @project.team << [@user, :reporter] end @@ -18,7 +18,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I already have a project named "Shop" in my namespace' do - @my_project = create(:project, name: "Shop", namespace: current_user.namespace) + @my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace) end step 'I should see a "Name has already been taken" warning' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 6c14d83500401e817f0e41ae2b2406bf03ef60c7..c0827ff8fc7537bf5470a8c879d1364fd53c8dba 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -7,7 +7,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps step 'I am a member of project "Shop"' do @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop") + @project ||= create(:project, :repository, name: "Shop") @project.team << [@user, :reporter] end diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 4fda0731e2f21d592a11f24595e7c75c5ed82085..0a3f4649870992598d8cc19a03f0f2ff5b3b96d2 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -28,7 +28,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps step 'There is an open Merge Request' do @user = create(:user) - @project = create(:project, :public) + @project = create(:project, :public, :repository) @project_member = create(:project_member, :developer, user: @user, project: @project) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) end diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb index efbc4831ce1e596ee31af7d5dd6561181d519a9d..3cc4fe9dafbed44e4864fd3bb5cdfd76ac31626c 100644 --- a/features/steps/project/merge_requests/revert.rb +++ b/features/steps/project/merge_requests/revert.rb @@ -35,7 +35,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps step 'There is an open Merge Request' do @user = create(:user) - @project = create(:project, :public) + @project = create(:project, :public, :repository) @project_member = create(:project_member, :developer, user: @user, project: @project) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) end diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index 1ffd5cb9de55f2a868156290d320214a9c7127d1..92936f27c2054ec9b56801698494876d6c872530 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -4,11 +4,11 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps include SharedProject step 'public project "Community"' do - create :project, :public, name: 'Community' + create(:empty_project, :public, name: 'Community') end step 'private project "Enterprise"' do - create :project, name: 'Enterprise' + create(:empty_project, :private, name: 'Enterprise') end step 'I visit project "Community" page' do diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index bd6466f3686af1dd75ed45ed7a5318ed4e526f93..a4d29770922c78878a51c5dd11434f05f8a34d72 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -137,17 +137,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Colorize messages').value).to eq '1' end - step 'I click Slack service link' do - click_link 'Slack' + step 'I click Slack notifications service link' do + click_link 'Slack notifications' end - step 'I fill Slack settings' do + step 'I fill Slack notifications settings' do check 'Active' fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' click_button 'Save' end - step 'I should see Slack service settings saved' do + step 'I should see Slack Notifications service settings saved' do expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 1cc9e37b0750179d83b3cc1bb85301aacefe9c96..f18adcadcce1e2cf755584fba4f2350f2d53c419 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -6,7 +6,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include RepoHelpers step "I don't have write access" do - @project = create(:project, name: "Other Project", path: "other-project") + @project = create(:project, :repository, name: "Other Project", path: "other-project") @project.team << [@user, :reporter] visit namespace_project_tree_path(@project.namespace, @project, root_ref) end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index dee6a8a5558e97d4b36c787c5f27105e9b72a121..9183de76881506f60d847b14426d3175dadafea7 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -8,7 +8,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I own project "Delta"' do @project = Project.find_by(name: "Delta") - @project ||= create(:project, name: "Delta", namespace: @user.namespace) + @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace) @project.team << [@user, :master] end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 22d971fadfb594aca7f3a0f790bc81ea53e884fb..6986c7ede566d4346f90d1e735de4564405adb1c 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -113,8 +113,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps project.team << [user, :reporter] end - step 'I click link "Import team from another project"' do - click_link "Import" + step 'I click link "Import team from another project"' do + page.within '.users-project-form' do + click_link "Import" + end end When 'I submit "Website" project for import team' do @@ -135,7 +137,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I share project with group "OpenSource"' do project = Project.find_by(name: 'Shop') os_group = create(:group, name: 'OpenSource') - create(:project, group: os_group) + create(:empty_project, group: os_group) @os_user1 = create(:user) @os_user2 = create(:user) os_group.add_owner(@os_user1) diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb index fbaa408226e70ac7e6a6c5b994d658cc70b2f3d7..ac0a17641474ec6726b21ad30b6f54705642691e 100644 --- a/features/steps/shared/admin.rb +++ b/features/steps/shared/admin.rb @@ -2,7 +2,7 @@ module SharedAdmin include Spinach::DSL step 'there are projects in system' do - 2.times { create(:project) } + 2.times { create(:project, :repository) } end step 'system has users' do diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 735e0ef6108a6945ba466bc04d73378f7955d5e8..5c3e724746b069db1bc79bf401820e1f9d18f665 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -33,6 +33,6 @@ module SharedAuthentication end def current_user - @user || User.first + @user || User.reorder(nil).first end end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index fe6736dacd42989ff9b1919244e2939e5fa7d114..de119f2c6c07e28362d2f9cbe917a65960e900ef 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -40,7 +40,7 @@ module SharedGroup user = User.find_by(name: username) || create(:user, name: username) group = Group.find_by(name: groupname) || create(:group, name: groupname) group.add_user(user, role) - project ||= create(:project, namespace: group, path: "project#{@project_count}") + project ||= create(:project, :repository, namespace: group, path: "project#{@project_count}") create(:closed_issue_event, project: project) project.team << [user, :master] @project_count += 1 diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index d36eff5cf16d647bdec52741fcc35b3c07e37538..670e6ca49a3ed10e2d5d1c00fc208ae14438af69 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -168,7 +168,7 @@ module SharedPaths end step 'I visit admin projects page' do - visit admin_namespaces_projects_path + visit admin_projects_path end step 'I visit admin users page' do @@ -191,14 +191,6 @@ module SharedPaths visit admin_background_jobs_path end - step 'I visit admin groups page' do - visit admin_groups_path - end - - step 'I visit admin appearance page' do - visit admin_appearances_path - end - step 'I visit admin teams page' do visit admin_teams_path end @@ -207,10 +199,6 @@ module SharedPaths visit admin_spam_logs_path end - step 'I visit applications page' do - visit admin_applications_path - end - # ---------------------------------------- # Generic Project # ---------------------------------------- diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b51152c79c6d77f68503b9d90c5e5069b411a684..553de0345d519b6553e06d9b98bd09467df94139 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -3,19 +3,19 @@ module SharedProject # Create a project without caring about what it's called step "I own a project" do - @project = create(:project, namespace: @user.namespace) + @project = create(:project, :repository, namespace: @user.namespace) @project.team << [@user, :master] end step "project exists in some group namespace" do @group = create(:group, name: 'some group') - @project = create(:project, namespace: @group, public_builds: false) + @project = create(:project, :repository, namespace: @group, public_builds: false) end # Create a specific project called "Shop" step 'I own project "Shop"' do @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop", namespace: @user.namespace) + @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace) @project.team << [@user, :master] end @@ -40,7 +40,7 @@ module SharedProject # Create another specific project called "Forum" step 'I own project "Forum"' do @project = Project.find_by(name: "Forum") - @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project') + @project ||= create(:project, :repository, name: "Forum", namespace: @user.namespace, path: 'forum_project') @project.build_project_feature @project.project_feature.save @project.team << [@user, :master] @@ -121,7 +121,7 @@ module SharedProject # ---------------------------------------- step 'archived project "Archive"' do - create :project, :public, archived: true, name: 'Archive' + create(:project, :archived, :public, :repository, name: 'Archive') end step 'I should not see project "Archive"' do @@ -144,7 +144,7 @@ module SharedProject # ---------------------------------------- step 'private project "Enterprise"' do - create :project, name: 'Enterprise' + create(:project, :private, :repository, name: 'Enterprise') end step 'I should see project "Enterprise"' do @@ -156,7 +156,7 @@ module SharedProject end step 'internal project "Internal"' do - create :project, :internal, name: 'Internal' + create(:project, :internal, :repository, name: 'Internal') end step 'I should see project "Internal"' do @@ -168,7 +168,7 @@ module SharedProject end step 'public project "Community"' do - create :project, :public, name: 'Community' + create(:project, :public, :repository, name: 'Community') end step 'I should see project "Community"' do diff --git a/features/steps/user.rb b/features/steps/user.rb index 59385a6ab59d03ec4552f9449f75fc048e2528ca..271c9b097d409fcac8f96e4d2cdaffbb01e7093a 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -38,6 +38,6 @@ class Spinach::Features::User < Spinach::FeatureSteps end def contributed_project - @contributed_project ||= create(:project, :public) + @contributed_project ||= create(:empty_project, :public) end end diff --git a/lib/api/api.rb b/lib/api/api.rb index cec2702e44d91fe71f39dee9c3177914c75895c9..6cf6b501021925b01bb495eaf059b02590253e53 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,6 +3,8 @@ module API include APIGuard version 'v3', using: :path + before { allow_access_with_scope :api } + rescue_from Gitlab::Access::AccessDeniedError do rack_response({ 'message' => '403 Forbidden' }.to_json, 403) end @@ -12,7 +14,11 @@ module API end # Retain 405 error rather than a 500 error for Grape 0.15.0+. - # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de + # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes + rescue_from Grape::Exceptions::MethodNotAllowed do |e| + error! e.message, e.status, e.headers + end + rescue_from Grape::Exceptions::Base do |e| error! e.message, e.status, e.headers end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 8cc7a26f1fa71249e79d28f71308a5fdc097a251..df6db140d0e724e6292bbcce49a745054da8ea54 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -6,6 +6,9 @@ module API module APIGuard extend ActiveSupport::Concern + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" + PRIVATE_TOKEN_PARAM = :private_token + included do |base| # OAuth2 Resource Server Authentication use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| @@ -44,27 +47,60 @@ module API access_token = find_access_token return nil unless access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + case AccessTokenValidationService.new(access_token).validate(scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED + when AccessTokenValidationService::EXPIRED raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED + when AccessTokenValidationService::REVOKED raise RevokedError - when Oauth2::AccessTokenValidationService::VALID + when AccessTokenValidationService::VALID @current_user = User.find(access_token.resource_owner_id) end end + def find_user_by_private_token(scopes: []) + token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s + + return nil unless token_string.present? + + find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes) + end + def current_user @current_user end + # Set the authorization scope(s) allowed for the current request. + # + # Note: A call to this method adds to any previous scopes in place. This is done because + # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then + # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the + # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they + # need to be stored. + def allow_access_with_scope(*scopes) + @scopes ||= [] + @scopes.concat(scopes.map(&:to_s)) + end + private + def find_user_by_authentication_token(token_string) + User.find_by_authentication_token(token_string) + end + + def find_user_by_personal_access_token(token_string, scopes) + access_token = PersonalAccessToken.active.find_by_token(token_string) + return unless access_token + + if AccessTokenValidationService.new(access_token).include_any_scope?(scopes) + User.find(access_token.user_id) + end + end + def find_access_token @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) end @@ -72,10 +108,6 @@ module API def doorkeeper_request @doorkeeper_request ||= ActionDispatch::Request.new(env) end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end end module ClassMethods diff --git a/lib/api/commits.rb b/lib/api/commits.rb index cf2489dbb6798aac4e79749f4221807341c5e093..e6d707f3c3d7cd1217a150db40b33b4060196a07 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -44,7 +44,6 @@ module API detail 'This feature was introduced in GitLab 8.13' end params do - requires :id, type: Integer, desc: 'The project ID' requires :branch_name, type: String, desc: 'The name of branch' requires :commit_message, type: String, desc: 'Commit message' requires :actions, type: Array[Hash], desc: 'Actions to perform in commit' diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 01c0f5072ba40dc1b29fc4b5996989e5a9ecdc03..9f59939e9ae041dfefd855cb3beb31e52c76446e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -78,21 +78,21 @@ module API expose :container_registry_enabled # Expose old field names with the new permissions methods to keep API compatible - expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) } - expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) } - expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) } - expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) } - expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) } + expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } + expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } + expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } expose :created_at, :last_activity_at expose :shared_runners_enabled expose :lfs_enabled?, as: :lfs_enabled expose :creator_id - expose :namespace + expose :namespace, using: 'API::Entities::Namespace' expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } expose :avatar_url expose :star_count, :forks_count - expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? } + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds expose :shared_with_groups do |project, options| @@ -101,6 +101,16 @@ module API expose :only_allow_merge_if_build_succeeds expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved + + expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + end + + class ProjectStatistics < Grape::Entity + expose :commit_count + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size end class Member < UserBasic @@ -127,6 +137,15 @@ module API expose :avatar_url expose :web_url expose :request_access_enabled + + expose :statistics, if: :statistics do + with_options format_with: -> (value) { value.to_i } do + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size + end + end end class GroupDetail < Group @@ -249,6 +268,13 @@ module API end end + class IssuableTimeStats < Grape::Entity + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent + end + class ExternalIssue < Grape::Entity expose :title expose :id @@ -298,7 +324,7 @@ module API end class SSHKey < Grape::Entity - expose :id, :title, :key, :created_at + expose :id, :title, :key, :created_at, :can_push end class SSHKeyWithUser < SSHKey @@ -391,7 +417,7 @@ module API end class Namespace < Grape::Entity - expose :id, :path, :kind + expose :id, :name, :path, :kind end class MemberAccess < Grape::Entity @@ -440,12 +466,12 @@ module API class ProjectWithAccess < Project expose :permissions do expose :project_access, using: Entities::ProjectAccess do |project, options| - project.project_members.find_by(user_id: options[:user].id) + project.project_members.find_by(user_id: options[:current_user].id) end expose :group_access, using: Entities::GroupAccess do |project, options| if project.group - project.group.group_members.find_by(user_id: options[:user].id) + project.group.group_members.find_by(user_id: options[:current_user].id) end end end @@ -546,6 +572,8 @@ module API expose :repository_storages expose :koding_enabled expose :koding_url + expose :plantuml_enabled + expose :plantuml_url end class Release < Grape::Entity @@ -629,7 +657,7 @@ module API end class EnvironmentBasic < Grape::Entity - expose :id, :name, :external_url + expose :id, :name, :slug, :external_url end class Environment < EnvironmentBasic diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 80bbd9bb6e4d4fb963ba61f3186cb4edf439581d..1a7e68f0528f3d15f628d48fb3a47c824ce3c2e5 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -1,6 +1,7 @@ module API # Environments RESTfull API endpoints class Environments < Grape::API + include ::API::Helpers::CustomValidators include PaginationParams before { authenticate! } @@ -29,6 +30,7 @@ module API params do requires :name, type: String, desc: 'The name of the environment to be created' optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } end post ':id/environments' do authorize! :create_environment, user_project @@ -50,6 +52,7 @@ module API requires :environment_id, type: Integer, desc: 'The environment ID' optional :name, type: String, desc: 'The new environment name' optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' + optional :slug, absence: { message: "is automatically generated and cannot be changed" } end put ':id/environments/:environment_id' do authorize! :update_environment, user_project diff --git a/lib/api/files.rb b/lib/api/files.rb index 28f306e45f3fb06ef5a8ac2f33537b2e6b413848..2e79e22e649996fc2f55c9cc7077d0362bbb0e6b 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,8 +1,6 @@ module API # Projects API class Files < Grape::API - before { authenticate! } - helpers do def commit_params(attrs) { @@ -70,7 +68,7 @@ module API ref: params[:ref], blob_id: blob.id, commit_id: commit.id, - last_commit_id: repo.last_commit_for_path(commit.sha, params[:file_path]).id + last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path]) } end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9b9d3df74359a97aee4dceda1588b92d0a116077..7682d2868665f08fd93812fe5d74cf909a9d05cb 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -11,6 +11,20 @@ module API optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' end + + params :statistics_params do + optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' + end + + def present_groups(groups, options = {}) + options = options.reverse_merge( + with: Entities::Group, + current_user: current_user, + ) + + groups = groups.with_statistics if options[:statistics] + present paginate(groups), options + end end resource :groups do @@ -18,6 +32,7 @@ module API success Entities::Group end params do + use :statistics_params optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :search, type: String, desc: 'Search for a specific group' @@ -38,7 +53,7 @@ module API groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.reorder(params[:order_by] => params[:sort]) - present paginate(groups), with: Entities::Group + present_groups groups, statistics: params[:statistics] && current_user.is_admin? end desc 'Get list of owned groups for authenticated user' do @@ -46,10 +61,10 @@ module API end params do use :pagination + use :statistics_params end get '/owned' do - groups = current_user.owned_groups - present paginate(groups), with: Entities::Group, user: current_user + present_groups current_user.owned_groups, statistics: params[:statistics] end desc 'Create a group. Available only for users who can create groups.' do @@ -66,7 +81,7 @@ module API group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute if group.persisted? - present group, with: Entities::Group + present group, with: Entities::Group, current_user: current_user else render_api_error!("Failed to save group #{group.errors.messages}", 400) end @@ -92,7 +107,7 @@ module API authorize! :admin_group, group if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute - present group, with: Entities::GroupDetail + present group, with: Entities::GroupDetail, current_user: current_user else render_validation_error!(group) end @@ -103,7 +118,7 @@ module API end get ":id" do group = find_group!(params[:id]) - present group, with: Entities::GroupDetail + present group, with: Entities::GroupDetail, current_user: current_user end desc 'Remove a group.' @@ -134,23 +149,23 @@ module API projects = GroupProjectsFinder.new(group).execute(current_user) projects = filter_projects(projects) entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project - present paginate(projects), with: entity, user: current_user + present paginate(projects), with: entity, current_user: current_user end desc 'Transfer a project to the group namespace. Available only for admin.' do success Entities::GroupDetail end params do - requires :project_id, type: String, desc: 'The ID of the project' + requires :project_id, type: String, desc: 'The ID or path of the project' end post ":id/projects/:project_id" do authenticated_as_admin! - group = Group.find_by(id: params[:id]) - project = Project.find(params[:project_id]) + group = find_group!(params[:id]) + project = find_project!(params[:project_id]) result = ::Projects::TransferService.new(project, current_user).execute(group) if result - present group, with: Entities::GroupDetail + present group, with: Entities::GroupDetail, current_user: current_user else render_api_error!("Failed to transfer project #{project.errors.messages}", 400) end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3324001c46810d973b12535b3d5f11a57a0678a5..49c5f0652ab58bc8921e7ed87d034e0ec7845863 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,9 +1,8 @@ module API module Helpers include Gitlab::Utils + include Helpers::Pagination - PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" - PRIVATE_TOKEN_PARAM = :private_token SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo @@ -87,10 +86,8 @@ module API IssuesFinder.new(current_user, project_id: user_project.id).find(id) end - def paginate(relation) - relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| - add_pagination_headers(data) - end + def find_project_merge_request(id) + MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) end def authenticate! @@ -98,7 +95,7 @@ module API end def authenticate_non_get! - authenticate! unless %w[GET HEAD].include?(route.route_method) + authenticate! unless %w[GET HEAD].include?(route.request_method) end def authenticate_by_gitlab_shell_token! @@ -250,7 +247,7 @@ module API rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) end - # Projects helpers + # project helpers def filter_projects(projects) if params[:search].present? @@ -301,14 +298,14 @@ module API header['X-Sendfile'] = path body else - file FileStreamer.new(path) + path end end private def private_token - params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] + params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER] end def warden @@ -323,18 +320,11 @@ module API warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) end - def find_user_by_private_token - token = private_token - return nil unless token.present? - - User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) - end - def initial_current_user return @initial_current_user if defined?(@initial_current_user) - @initial_current_user ||= find_user_by_private_token - @initial_current_user ||= doorkeeper_guard + @initial_current_user ||= find_user_by_private_token(scopes: @scopes) + @initial_current_user ||= doorkeeper_guard(scopes: @scopes) @initial_current_user ||= find_user_from_warden unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? @@ -370,38 +360,6 @@ module API @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] end - def add_pagination_headers(paginated_data) - header 'X-Total', paginated_data.total_count.to_s - header 'X-Total-Pages', paginated_data.total_pages.to_s - header 'X-Per-Page', paginated_data.limit_value.to_s - header 'X-Page', paginated_data.current_page.to_s - header 'X-Next-Page', paginated_data.next_page.to_s - header 'X-Prev-Page', paginated_data.prev_page.to_s - header 'Link', pagination_links(paginated_data) - end - - def pagination_links(paginated_data) - request_url = request.url.split('?').first - request_params = params.clone - request_params[:per_page] = paginated_data.limit_value - - links = [] - - request_params[:page] = paginated_data.current_page - 1 - links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page? - - request_params[:page] = paginated_data.current_page + 1 - links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page? - - request_params[:page] = 1 - links << %(<#{request_url}?#{request_params.to_query}>; rel="first") - - request_params[:page] = paginated_data.total_pages - links << %(<#{request_url}?#{request_params.to_query}>; rel="last") - - links.join(', ') - end - def secret_token Gitlab::Shell.secret_token end diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb new file mode 100644 index 0000000000000000000000000000000000000000..0a8f3073a50e1fc191e6eeee21ea9314d2b93f32 --- /dev/null +++ b/lib/api/helpers/custom_validators.rb @@ -0,0 +1,14 @@ +module API + module Helpers + module CustomValidators + class Absence < Grape::Validations::Base + def validate_param!(attr_name, params) + return if params.respond_to?(:key?) && !params.key?(attr_name) + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) + end + end + end + end +end + +Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index eb223c1101db27c0d914a552cc4f5ed0fc880249..e8975eb57e0f05e98d2b611cd118fd9c1732802f 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -52,6 +52,14 @@ module API :push_code ] end + + def parse_allowed_environment_variables + return if params[:env].blank? + + JSON.parse(params[:env]) + + rescue JSON::ParserError + end end end end diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb new file mode 100644 index 0000000000000000000000000000000000000000..2199eea7e5ffa40b78c86f8aeee35555de4c24b5 --- /dev/null +++ b/lib/api/helpers/pagination.rb @@ -0,0 +1,45 @@ +module API + module Helpers + module Pagination + def paginate(relation) + relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| + add_pagination_headers(data) + end + end + + private + + def add_pagination_headers(paginated_data) + header 'X-Total', paginated_data.total_count.to_s + header 'X-Total-Pages', paginated_data.total_pages.to_s + header 'X-Per-Page', paginated_data.limit_value.to_s + header 'X-Page', paginated_data.current_page.to_s + header 'X-Next-Page', paginated_data.next_page.to_s + header 'X-Prev-Page', paginated_data.prev_page.to_s + header 'Link', pagination_links(paginated_data) + end + + def pagination_links(paginated_data) + request_url = request.url.split('?').first + request_params = params.clone + request_params[:per_page] = paginated_data.limit_value + + links = [] + + request_params[:page] = paginated_data.current_page - 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page? + + request_params[:page] = paginated_data.current_page + 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page? + + request_params[:page] = 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="first") + + request_params[:page] = paginated_data.total_pages + links << %(<#{request_url}?#{request_params.to_query}>; rel="last") + + links.join(', ') + end + end + end +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 7087ce114014f325dfa23c66897fa096c80942d7..d235977fbd843d136a06e2e4646583d28a35599e 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -28,11 +28,17 @@ module API protocol = params[:protocol] + actor.update_last_used_at if actor.is_a?(Key) + access = if wiki? Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) else - Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) + Gitlab::GitAccess.new(actor, + project, + protocol, + authentication_abilities: ssh_authentication_abilities, + env: parse_allowed_environment_variables) end access_status = access.check(params[:action], params[:changes]) @@ -57,6 +63,8 @@ module API status 200 key = Key.find(params[:key_id]) + key.update_last_used_at + token_handler = Gitlab::LfsToken.new(key) { @@ -99,7 +107,9 @@ module API key = Key.find_by(id: params[:key_id]) - unless key + if key + key.update_last_used_at + else return { 'success' => false, 'message' => 'Could not find the given key' } end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c9124649cbbf34f51908f54e958078f8ae2248cc..fe016c1ec0a06d3aeb896040a53a64e435ea3772 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -5,28 +5,36 @@ module API before { authenticate! } helpers do - def filter_issues_state(issues, state) - case state - when 'opened' then issues.opened - when 'closed' then issues.closed - else issues - end - end + def find_issues(args = {}) + args = params.merge(args) - def filter_issues_labels(issues, labels) - issues.includes(:labels).where('labels.title' => labels.split(',')) - end + args.delete(:id) + args[:milestone_title] = args.delete(:milestone) + + match_all_labels = args.delete(:match_all_labels) + labels = args.delete(:labels) + args[:label_name] = labels if match_all_labels + + args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid) + + issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations - def filter_issues_milestone(issues, milestone) - issues.includes(:milestone).where('milestones.title' => milestone) + # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder + if !match_all_labels && labels.present? + issues = issues.includes(:labels).where('labels.title' => labels.split(',')) + end + + issues.reorder(args[:order_by] => args[:sort]) end params :issues_params do optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :milestone, type: String, desc: 'Milestone title' optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', desc: 'Return issues ordered by `created_at` or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return issues sorted in `asc` or `desc` order.' + optional :milestone, type: String, desc: 'Return issues for a specific milestone' use :pagination end @@ -37,8 +45,6 @@ module API optional :labels, type: String, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' - optional :state_event, type: String, values: %w[open close], - desc: 'State of the issue' end end @@ -52,10 +58,7 @@ module API use :issues_params end get do - issues = current_user.issues.inc_notes_with_associations - issues = filter_issues_state(issues, params[:state]) - issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = issues.reorder(params[:order_by] => params[:sort]) + issues = find_issues(scope: 'authored') present paginate(issues), with: Entities::Issue, current_user: current_user end @@ -74,15 +77,10 @@ module API use :issues_params end get ":id/issues" do - group = find_group!(params.delete(:id)) - - params[:group_id] = group.id - params[:milestone_title] = params.delete(:milestone) - params[:label_name] = params.delete(:labels) + group = find_group!(params[:id]) - issues = IssuesFinder.new(current_user, params).execute + issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true) - issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user end end @@ -91,26 +89,22 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects do + include TimeTrackingEndpoints + desc 'Get a list of project issues' do success Entities::Issue end params do optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' - optional :iid, type: Integer, desc: 'The IID of the issue' + optional :iid, type: Integer, desc: 'Return the issue having the given `iid`' use :issues_params end get ":id/issues" do - issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations - issues = filter_issues_state(issues, params[:state]) - issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? + project = find_project(params[:id]) - unless params[:milestone].nil? - issues = filter_issues_milestone(issues, params[:milestone]) - end + issues = find_issues(project_id: project.id) - issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project end @@ -172,6 +166,7 @@ module API optional :title, type: String, desc: 'The title of an issue' optional :updated_at, type: DateTime, desc: 'Date time when the issue was updated. Available only for admins and project owners.' + optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' use :issue_params at_least_one_of :title, :description, :assignee_id, :milestone_id, :labels, :created_at, :due_date, :confidential, :state_event diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 5d1fe22f2dff68597fa1bc08850479f2bb3ee2f9..e77af4b7a0d377d0b13170960187ad013d8874c0 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -10,6 +10,8 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects do + include TimeTrackingEndpoints + helpers do def handle_merge_request_errors!(errors) if errors[:project_access].any? @@ -96,7 +98,7 @@ module API requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' end delete ":id/merge_requests/:merge_request_id" do - merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize!(:destroy_merge_request, merge_request) merge_request.destroy @@ -116,7 +118,7 @@ module API success Entities::MergeRequest end get path do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end @@ -125,7 +127,7 @@ module API success Entities::RepoCommit end get "#{path}/commits" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request present merge_request.commits, with: Entities::RepoCommit end @@ -134,7 +136,7 @@ module API success Entities::MergeRequestChanges end get "#{path}/changes" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end @@ -153,7 +155,7 @@ module API :remove_source_branch end put path do - merge_request = user_project.merge_requests.find(params.delete(:merge_request_id)) + merge_request = find_project_merge_request(params.delete(:merge_request_id)) authorize! :update_merge_request, merge_request mr_params = declared_params(include_missing: false) @@ -180,7 +182,7 @@ module API optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' end put "#{path}/merge" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) # Merge request can not be merged # because user dont have permissions to push into target branch @@ -216,7 +218,7 @@ module API success Entities::MergeRequest end post "#{path}/cancel_merge_when_build_succeeds" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) @@ -233,7 +235,7 @@ module API use :pagination end get "#{path}/comments" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request @@ -248,7 +250,7 @@ module API requires :note, type: String, desc: 'The text of the comment' end post "#{path}/comments" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :create_note, merge_request opts = { @@ -273,7 +275,7 @@ module API use :pagination end get "#{path}/closes_issues" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: issue_entity(user_project), current_user: current_user end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index d0faf17714b1f296e60975de07acf13632d313d0..284e4cf549ad9fb7920046c7dee90eaced1116fa 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -69,8 +69,6 @@ module API optional :created_at, type: String, desc: 'The creation date of the note' end post ":id/#{noteables_str}/:noteable_id/notes" do - required_attributes! [:body] - opts = { note: params[:body], noteable_type: noteables_str.classify, diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index dcc0fb7a91185b31a245efc7755b0a9e1202bf14..cb679e6658a3085a7e61f53fd8555a3f56ae3cc6 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -15,7 +15,7 @@ module API optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" optional :build_events, type: Boolean, desc: "Trigger hook on build events" optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" - optional :wiki_events, type: Boolean, desc: "Trigger hook on wiki events" + optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 2929d2157dc72b16fac0b4fdb5c22b189f3e20cd..941f47114a4554672e16c7b08a578bc139640d46 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -40,6 +40,15 @@ module API resource :projects do helpers do + params :collection_params do + use :sort_params + use :filter_params + use :pagination + + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' + end + params :sort_params do optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], default: 'created_at', desc: 'Return projects ordered by field' @@ -52,97 +61,94 @@ module API optional :visibility, type: String, values: %w[public internal private], desc: 'Limit by visibility' optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' - use :sort_params + end + + params :statistics_params do + optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' end params :create_params do optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' optional :import_url, type: String, desc: 'URL from which the project is imported' end + + def present_projects(projects, options = {}) + options = options.reverse_merge( + with: Entities::Project, + current_user: current_user, + simple: params[:simple], + ) + + projects = filter_projects(projects) + projects = projects.with_statistics if options[:statistics] + options[:with] = Entities::BasicProjectDetails if options[:simple] + + present paginate(projects), options + end end desc 'Get a list of visible projects for authenticated user' do success Entities::BasicProjectDetails end params do - optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' - use :filter_params - use :pagination + use :collection_params end get '/visible' do - projects = ProjectsFinder.new.execute(current_user) - projects = filter_projects(projects) - entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess - - present paginate(projects), with: entity, user: current_user + entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + present_projects ProjectsFinder.new.execute(current_user), with: entity end desc 'Get a projects list for authenticated user' do success Entities::BasicProjectDetails end params do - optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' - use :filter_params - use :pagination + use :collection_params end get do authenticate! - projects = current_user.authorized_projects - projects = filter_projects(projects) - entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess - - present paginate(projects), with: entity, user: current_user + present_projects current_user.authorized_projects, + with: Entities::ProjectWithAccess end desc 'Get an owned projects list for authenticated user' do success Entities::BasicProjectDetails end params do - use :filter_params - use :pagination + use :collection_params + use :statistics_params end get '/owned' do authenticate! - projects = current_user.owned_projects - projects = filter_projects(projects) - - present paginate(projects), with: Entities::ProjectWithAccess, user: current_user + present_projects current_user.owned_projects, + with: Entities::ProjectWithAccess, + statistics: params[:statistics] end desc 'Gets starred project for the authenticated user' do success Entities::BasicProjectDetails end params do - use :filter_params - use :pagination + use :collection_params end get '/starred' do authenticate! - projects = current_user.viewable_starred_projects - projects = filter_projects(projects) - - present paginate(projects), with: Entities::Project, user: current_user + present_projects current_user.viewable_starred_projects end desc 'Get all projects for admin user' do success Entities::BasicProjectDetails end params do - use :filter_params - use :pagination + use :collection_params + use :statistics_params end get '/all' do authenticated_as_admin! - projects = Project.all - projects = filter_projects(projects) - - present paginate(projects), with: Entities::ProjectWithAccess, user: current_user + present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics] end desc 'Search for projects the current user has access to' do @@ -153,7 +159,7 @@ module API use :sort_params use :pagination end - get "/search/:query" do + get "/search/:query", requirements: { query: /[^\/]+/ } do search_service = Search::GlobalService.new(current_user, search: params[:query]).execute projects = search_service.objects('projects', params[:page]) projects = projects.reorder(params[:order_by] => params[:sort]) @@ -221,7 +227,7 @@ module API end get ":id" do entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails - present user_project, with: entity, user: current_user, + present user_project, with: entity, current_user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project) end @@ -289,13 +295,13 @@ module API authorize! :rename_project, user_project if attrs[:name].present? authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? - ::Projects::UpdateService.new(user_project, current_user, attrs).execute + result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute - if user_project.errors.any? - render_validation_error!(user_project) - else + if result[:status] == :success present user_project, with: Entities::Project, user_can_admin_project: can?(current_user, :admin_project, user_project) + else + render_validation_error!(user_project) end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index c287ee34a6818018ae858944ddb1158bc48c0786..4ca6646a6f1e99516bb8dab8e0ea5bd38e187203 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -2,7 +2,6 @@ require 'mime/types' module API class Repositories < Grape::API - before { authenticate! } before { authorize! :download_code, user_project } params do @@ -79,8 +78,6 @@ module API optional :format, type: String, desc: 'The archive format' end get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do - authorize! :download_code, user_project - begin send_git_archive user_project.repository, ref: params[:sha], format: params[:format] rescue @@ -96,7 +93,6 @@ module API requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' end get ':id/repository/compare' do - authorize! :download_code, user_project compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) present compare, with: Entities::Compare end @@ -105,8 +101,6 @@ module API success Entities::Contributor end get ':id/repository/contributors' do - authorize! :download_code, user_project - begin present user_project.repository.contributors, with: Entities::Contributor diff --git a/lib/api/services.rb b/lib/api/services.rb index fde2e2746f1bb1cc12fc1982d3de9049e447c5ba..3a9dfbb237c3e501b6b58b6e478a1be09bd8d74b 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -351,6 +351,33 @@ module API desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' } ], + + 'kubernetes' => [ + { + required: true, + name: :namespace, + type: String, + desc: 'The Kubernetes namespace to use' + }, + { + required: true, + name: :api_url, + type: String, + desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' + }, + { + required: true, + name: :token, + type: String, + desc: 'The service token to authenticate against the Kubernetes cluster with' + }, + { + required: false, + name: :ca_pem, + type: String, + desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' + }, + ], 'mattermost-slash-commands' => [ { required: true, @@ -359,6 +386,14 @@ module API desc: 'The Mattermost token' } ], + 'slack-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Slack token' + } + ], 'pipelines-email' => [ { required: true, @@ -465,6 +500,14 @@ module API desc: 'The channel name' } ], + 'mattermost' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' + } + ], 'teamcity' => [ { required: true, @@ -500,6 +543,13 @@ module API type: String, desc: 'The Mattermost token' } + ], + 'slack-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Slack token' + } ] }.freeze diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c4cb1c7924ab534fdd6b16f49a7ff2b794f13fdd..c5eff16a5de222f8e0c1de18c195ec967b1dc9ad 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -9,23 +9,121 @@ module API end end - # Get current applicaiton settings - # - # Example Request: - # GET /application/settings + desc 'Get the current application settings' do + success Entities::ApplicationSetting + end get "application/settings" do present current_settings, with: Entities::ApplicationSetting end - # Modify application settings - # - # Example Request: - # PUT /application/settings + desc 'Modify application settings' do + success Entities::ApplicationSetting + end + params do + optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' + optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility' + optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility' + optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility' + optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' + optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], + desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' + optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' + optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' + optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' + optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' + optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' + optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.' + optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider' + optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external' + optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled' + optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up' + optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' + given domain_blacklist_enabled: ->(val) { val } do + requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + end + optional :after_sign_up_text, type: String, desc: 'Text shown after sign up' + optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled' + optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication' + given require_two_factor_authentication: ->(val) { val } do + requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication' + end + optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' + optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' + optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application' + optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' + optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' + given shared_runners_enabled: ->(val) { val } do + requires :shared_runners_text, type: String, desc: 'Shared runners text ' + end + optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' + optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' + given metrics_enabled: ->(val) { val } do + requires :metrics_host, type: String, desc: 'The InfluxDB host' + requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB' + requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open' + requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out' + requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.' + requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds' + requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet' + end + optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling' + given sidekiq_throttling_enabled: ->(val) { val } do + requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle' + requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.' + end + optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts' + given recaptcha_enabled: ->(val) { val } do + requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' + requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' + end + optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues' + given akismet_enabled: ->(val) { val } do + requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' + end + optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' + optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com' + given sentry_enabled: ->(val) { val } do + requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name' + end + optional :repository_storage, type: String, desc: 'Storage paths for new projects' + optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." + optional :koding_enabled, type: Boolean, desc: 'Enable Koding' + given koding_enabled: ->(val) { val } do + requires :koding_url, type: String, desc: 'The Koding team URL' + end + optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML' + given plantuml_enabled: ->(val) { val } do + requires :plantuml_url, type: String, desc: 'The PlantUML server URL' + end + optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.' + optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' + optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' + optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' + given housekeeping_enabled: ->(val) { val } do + requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance." + requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." + requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." + requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." + end + at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, + :default_group_visibility, :restricted_visibility_levels, :import_sources, + :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, + :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources, + :user_oauth_applications, :user_default_external, :signup_enabled, + :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, + :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, + :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, + :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay, + :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, + :akismet_enabled, :admin_notification_email, :sentry_enabled, + :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, + :version_check_enabled, :email_author_in_body, :html_emails_enabled, + :housekeeping_enabled + end put "application/settings" do - attributes = ["repository_storage"] + current_settings.attributes.keys - ["id"] - attrs = attributes_for_keys(attributes) - - if current_settings.update_attributes(attrs) + if current_settings.update_attributes(declared_params(include_missing: false)) present current_settings, with: Entities::ApplicationSetting else render_validation_error!(current_settings) diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 8a53d9c0095d5355224bb19e40ce174b734b49f4..e23f99256a59e2fea3a74a1ae4c61438bab9ccce 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -8,6 +8,10 @@ module API gitlab_ci_ymls: { klass: Gitlab::Template::GitlabCiYmlTemplate, gitlab_version: 8.9 + }, + dockerfiles: { + klass: Gitlab::Template::DockerfileTemplate, + gitlab_version: 8.15 } }.freeze PROJECT_TEMPLATE_REGEX = @@ -51,7 +55,7 @@ module API end params do optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' - end + end get route do options = { featured: declared(params).popular.present? ? true : nil @@ -69,7 +73,7 @@ module API end params do requires :name, type: String, desc: 'The name of the template' - end + end get route, requirements: { name: /[\w\.-]+/ } do not_found!('License') unless Licensee::License.find(declared(params).name) @@ -78,7 +82,7 @@ module API present template, with: Entities::RepoLicense end end - + GLOBAL_TEMPLATE_TYPES.each do |template_type, properties| klass = properties[:klass] gitlab_version = properties[:gitlab_version] @@ -104,7 +108,7 @@ module API end params do requires :name, type: String, desc: 'The name of the template' - end + end get route do new_template = klass.find(declared(params).name) diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb new file mode 100644 index 0000000000000000000000000000000000000000..85b5f7d98b8d19ea793dd9b88121a22426eb91a9 --- /dev/null +++ b/lib/api/time_tracking_endpoints.rb @@ -0,0 +1,114 @@ +module API + module TimeTrackingEndpoints + extend ActiveSupport::Concern + + included do + helpers do + def issuable_name + declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request' + end + + def issuable_key + "#{issuable_name}_id".to_sym + end + + def update_issuable_key + "update_#{issuable_name}".to_sym + end + + def read_issuable_key + "read_#{issuable_name}".to_sym + end + + def load_issuable + @issuable ||= begin + case issuable_name + when 'issue' + find_project_issue(params.delete(issuable_key)) + when 'merge_request' + find_project_merge_request(params.delete(issuable_key)) + end + end + end + + def update_issuable(attrs) + custom_params = declared_params(include_missing: false) + custom_params.merge!(attrs) + + issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable) + if issuable.valid? + present issuable, with: Entities::IssuableTimeStats + else + render_validation_error!(issuable) + end + end + + def update_service + issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService + end + end + + issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' + issuable_collection_name = issuable_name.pluralize + issuable_key = "#{issuable_name}_id".to_sym + + desc "Set a time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) + end + + desc "Reset the time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: 0) + end + + desc "Add spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do + authorize! update_issuable_key, load_issuable + + update_issuable(spend_time: { + duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), + user: current_user + }) + end + + desc "Reset spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(spend_time: { duration: :reset, user: current_user }) + end + + desc "Show time stats for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do + authorize! read_issuable_key, load_issuable + + present load_issuable, with: Entities::IssuableTimeStats + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index c7db2d7101715d324dcb8ba82c71a32e5a9dd9b0..11a7368b4c0f4a6872be41764d1724725a59c4c0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -2,7 +2,10 @@ module API class Users < Grape::API include PaginationParams - before { authenticate! } + before do + allow_access_with_scope :read_user if request.get? + authenticate! + end resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do helpers do @@ -13,7 +16,7 @@ module API optional :website_url, type: String, desc: 'The website of the user' optional :organization, type: String, desc: 'The organization of the user' optional :projects_limit, type: Integer, desc: 'The number of projects a user can create' - optional :extern_uid, type: Integer, desc: 'The external authentication provider UID' + optional :extern_uid, type: String, desc: 'The external authentication provider UID' optional :provider, type: String, desc: 'The external provider' optional :bio, type: String, desc: 'The biography of the user' optional :location, type: String, desc: 'The location of the user' @@ -88,10 +91,11 @@ module API authenticated_as_admin! # Filter out params which are used later - identity_attrs = params.slice(:provider, :extern_uid) - confirm = params.delete(:confirm) + user_params = declared_params(include_missing: false) + identity_attrs = user_params.slice(:provider, :extern_uid) + confirm = user_params.delete(:confirm) - user = User.build_user(declared_params(include_missing: false)) + user = User.new(user_params.except(:extern_uid, :provider)) user.skip_confirmation! unless confirm if identity_attrs.any? @@ -156,11 +160,7 @@ module API end end - # Delete already handled parameters - user_params.delete(:extern_uid) - user_params.delete(:provider) - - if user.update_attributes(user_params) + if user.update_attributes(user_params.except(:extern_uid, :provider)) present user, with: Entities::UserPublic else render_validation_error!(user) diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 7e6537e3d9e218608fe5de7fb59ed9358969e051..cefbfdce3bba56c147d8a260c45954b22c45dd59 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -2,6 +2,7 @@ module Backup class Manager ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry] FOLDERS_TO_BACKUP = %w[repositories db] + FILE_NAME_SUFFIX = '_gitlab_backup.tar' def pack # Make sure there is a connection @@ -14,7 +15,7 @@ module Backup s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version s[:skipped] = ENV["SKIP"] - tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar' + tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}" Dir.chdir(Gitlab.config.backup.path) do File.open("#{Gitlab.config.backup.path}/backup_information.yml", @@ -82,7 +83,7 @@ module Backup removed = 0 Dir.chdir(Gitlab.config.backup.path) do - Dir.glob('*_gitlab_backup.tar').each do |file| + Dir.glob("*#{FILE_NAME_SUFFIX}").each do |file| next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/ timestamp = $1.to_i @@ -108,41 +109,50 @@ module Backup Dir.chdir(Gitlab.config.backup.path) # check for existing backups in the backup dir - file_list = Dir.glob("*_gitlab_backup.tar") - puts "no backups found" if file_list.count == 0 + file_list = Dir.glob("*#{FILE_NAME_SUFFIX}") + + if file_list.count == 0 + $progress.puts "No backups found in #{Gitlab.config.backup.path}" + $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}" + exit 1 + end if file_list.count > 1 && ENV["BACKUP"].nil? - puts "Found more than one backup, please specify which one you want to restore:" - puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" + $progress.puts 'Found more than one backup, please specify which one you want to restore:' + $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup' exit 1 end - tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first + if ENV['BACKUP'].present? + tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}" + else + tar_file = file_list.first + end unless File.exist?(tar_file) - puts "The specified backup doesn't exist!" + $progress.puts "The backup file #{tar_file} does not exist!" exit 1 end - $progress.print "Unpacking backup ... " + $progress.print 'Unpacking backup ... ' unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "unpacking backup failed".color(:red) + $progress.puts 'unpacking backup failed'.color(:red) exit 1 else - $progress.puts "done".color(:green) + $progress.puts 'done'.color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems if settings[:gitlab_version] != Gitlab::VERSION - puts "GitLab version mismatch:".color(:red) - puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) - puts " Please switch to the following version and try again:".color(:red) - puts " version: #{settings[:gitlab_version]}".color(:red) - puts - puts "Hint: git checkout v#{settings[:gitlab_version]}" + $progress.puts 'GitLab version mismatch:'.color(:red) + $progress.puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) + $progress.puts ' Please switch to the following version and try again:'.color(:red) + $progress.puts " version: #{settings[:gitlab_version]}".color(:red) + $progress.puts + $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}" exit 1 end end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index fd74eeaebe7df719b5a2698dc3536ad45bd8f987..6d04f68c8f9b5cd2f13c2a57f5c9dc47d97b63bf 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -254,15 +254,26 @@ module Banzai # Returns projects for the given paths. def find_projects_for_paths(paths) if RequestStore.active? - to_query = paths - project_refs_cache.keys + cache = project_refs_cache + to_query = paths - cache.keys unless to_query.empty? - projects_relation_for_paths(to_query).each do |project| - get_or_set_cache(project_refs_cache, project.path_with_namespace) { project } + projects = projects_relation_for_paths(to_query) + + found = [] + projects.each do |project| + ref = project.path_with_namespace + get_or_set_cache(cache, ref) { project } + found << ref + end + + not_found = to_query - found + not_found.each do |ref| + get_or_set_cache(cache, ref) { nil } end end - project_refs_cache.slice(*paths).values + cache.slice(*paths).values.compact else projects_relation_for_paths(paths) end diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 2f19b59e7252de0701adcfec839f412eb9415d27..d67d466bce85edc83c5cbd237f01984b2fb28ed8 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -10,7 +10,7 @@ module Banzai node.set_attribute('href', href) end - if href =~ /\Ahttp(s)?:\/\// && external_url?(href) + if href =~ %r{\A(https?:)?//[^/]} && external_url?(href) node.set_attribute('rel', 'nofollow noreferrer') node.set_attribute('target', '_blank') end diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb new file mode 100644 index 0000000000000000000000000000000000000000..b6e784c886ba8a65dae6122090078aa0300c81fc --- /dev/null +++ b/lib/banzai/filter/math_filter.rb @@ -0,0 +1,46 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. + # + class MathFilter < HTML::Pipeline::Filter + # Attribute indicating inline or display math. + STYLE_ATTRIBUTE = 'data-math-style'.freeze + + # Class used for tagging elements that should be rendered + TAG_CLASS = 'js-render-math'.freeze + + INLINE_CLASSES = "code math #{TAG_CLASS}".freeze + + DOLLAR_SIGN = '$'.freeze + + def call + doc.css('code').each do |code| + closing = code.next + opening = code.previous + + # We need a sibling before and after. + # They should end and start with $ respectively. + if closing && opening && + closing.text? && opening.text? && + closing.content.first == DOLLAR_SIGN && + opening.content.last == DOLLAR_SIGN + + code[:class] = INLINE_CLASSES + code[STYLE_ATTRIBUTE] = 'inline' + closing.content = closing.content[1..-1] + opening.content = opening.content[0..-2] + end + end + + doc.css('pre.code.math').each do |el| + el[STYLE_ATTRIBUTE] = 'display' + el[:class] += " #{TAG_CLASS}" + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 84bfeac80417a21ef7fcbcec13f9b1a5bfaafdf3..ab7af1cad2134d9c5958477c49050020a6f77f09 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -20,10 +20,10 @@ module Banzai # Examples: # # data_attribute(project: 1, issue: 2) - # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" # # data_attribute(project: 3, merge_request: 4) - # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" + # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String def data_attribute(attributes = {}) @@ -31,7 +31,9 @@ module Banzai attributes[:reference_type] ||= self.class.reference_type attributes.delete(:original) if context[:no_original_data] - attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") + attributes.map do |key, value| + %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") + end.join(' ') end def escape_once(html) diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5da2d0b008c860ad9c27c49506aabcc05ce2c6b4..5a1f873496cec2c8659f550844a687d05c95d408 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -6,6 +6,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::SanitizationFilter, + Filter::MathFilter, Filter::UploadLinkFilter, Filter::VideoLinkFilter, Filter::ImageLinkFilter, diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb new file mode 100644 index 0000000000000000000000000000000000000000..f8ee7e0f9aed5bd884f56ce1029781559c9f44f8 --- /dev/null +++ b/lib/bitbucket/client.rb @@ -0,0 +1,58 @@ +module Bitbucket + class Client + attr_reader :connection + + def initialize(options = {}) + @connection = Connection.new(options) + end + + def issues(repo) + path = "/repositories/#{repo}/issues" + get_collection(path, :issue) + end + + def issue_comments(repo, issue_id) + path = "/repositories/#{repo}/issues/#{issue_id}/comments" + get_collection(path, :comment) + end + + def pull_requests(repo) + path = "/repositories/#{repo}/pullrequests?state=ALL" + get_collection(path, :pull_request) + end + + def pull_request_comments(repo, pull_request) + path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments" + get_collection(path, :pull_request_comment) + end + + def pull_request_diff(repo, pull_request) + path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff" + connection.get(path) + end + + def repo(name) + parsed_response = connection.get("/repositories/#{name}") + Representation::Repo.new(parsed_response) + end + + def repos + path = "/repositories?role=member" + get_collection(path, :repo) + end + + def user + @user ||= begin + parsed_response = connection.get('/user') + Representation::User.new(parsed_response) + end + end + + private + + def get_collection(path, type) + paginator = Paginator.new(connection, path, type) + Collection.new(paginator) + end + end +end diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb new file mode 100644 index 0000000000000000000000000000000000000000..3a9379ff680b6e89531d72f1104167a7f40b4a41 --- /dev/null +++ b/lib/bitbucket/collection.rb @@ -0,0 +1,21 @@ +module Bitbucket + class Collection < Enumerator + def initialize(paginator) + super() do |yielder| + loop do + paginator.items.each { |item| yielder << item } + end + end + + lazy + end + + def method_missing(method, *args) + return super unless self.respond_to?(method) + + self.send(method, *args) do |item| + block_given? ? yield(item) : item + end + end + end +end diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb new file mode 100644 index 0000000000000000000000000000000000000000..7e55cf4deabfc2b338fdd12a76cc6e260b41bb68 --- /dev/null +++ b/lib/bitbucket/connection.rb @@ -0,0 +1,69 @@ +module Bitbucket + class Connection + DEFAULT_API_VERSION = '2.0' + DEFAULT_BASE_URI = 'https://api.bitbucket.org/' + DEFAULT_QUERY = {} + + attr_reader :expires_at, :expires_in, :refresh_token, :token + + def initialize(options = {}) + @api_version = options.fetch(:api_version, DEFAULT_API_VERSION) + @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI) + @default_query = options.fetch(:query, DEFAULT_QUERY) + + @token = options[:token] + @expires_at = options[:expires_at] + @expires_in = options[:expires_in] + @refresh_token = options[:refresh_token] + end + + def get(path, extra_query = {}) + refresh! if expired? + + response = connection.get(build_url(path), params: @default_query.merge(extra_query)) + response.parsed + end + + def expired? + connection.expired? + end + + def refresh! + response = connection.refresh! + + @token = response.token + @expires_at = response.expires_at + @expires_in = response.expires_in + @refresh_token = response.refresh_token + @connection = nil + end + + private + + def client + @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) + end + + def connection + @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in) + end + + def build_url(path) + return path if path.starts_with?(root_url) + + "#{root_url}#{path}" + end + + def root_url + @root_url ||= "#{@base_uri}#{@api_version}" + end + + def provider + Gitlab::OAuth::Provider.config_for('bitbucket') + end + + def options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys + end + end +end diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb new file mode 100644 index 0000000000000000000000000000000000000000..5e2eb57bb0ee37dad8158dcaf6bcf001e5e0ebf4 --- /dev/null +++ b/lib/bitbucket/error/unauthorized.rb @@ -0,0 +1,6 @@ +module Bitbucket + module Error + class Unauthorized < StandardError + end + end +end diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb new file mode 100644 index 0000000000000000000000000000000000000000..2b0a3fe7b1a88d136589c6970e022f4108cc9464 --- /dev/null +++ b/lib/bitbucket/page.rb @@ -0,0 +1,34 @@ +module Bitbucket + class Page + attr_reader :attrs, :items + + def initialize(raw, type) + @attrs = parse_attrs(raw) + @items = parse_values(raw, representation_class(type)) + end + + def next? + attrs.fetch(:next, false) + end + + def next + attrs.fetch(:next) + end + + private + + def parse_attrs(raw) + raw.slice(*%w(size page pagelen next previous)).symbolize_keys + end + + def parse_values(raw, bitbucket_rep_class) + return [] unless raw['values'] && raw['values'].is_a?(Array) + + bitbucket_rep_class.decorate(raw['values']) + end + + def representation_class(type) + Bitbucket::Representation.const_get(type.to_s.camelize) + end + end +end diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb new file mode 100644 index 0000000000000000000000000000000000000000..135d0d556743f658cfa54a05126cfe5f606e5f5c --- /dev/null +++ b/lib/bitbucket/paginator.rb @@ -0,0 +1,36 @@ +module Bitbucket + class Paginator + PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100. + + def initialize(connection, url, type) + @connection = connection + @type = type + @url = url + @page = nil + end + + def items + raise StopIteration unless has_next_page? + + @page = fetch_next_page + @page.items + end + + private + + attr_reader :connection, :page, :url, :type + + def has_next_page? + page.nil? || page.next? + end + + def next_url + page.nil? ? url : page.next + end + + def fetch_next_page + parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on) + Page.new(parsed_response, type) + end + end +end diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..94adaacc9b5598a85c2415ae8053e28380265892 --- /dev/null +++ b/lib/bitbucket/representation/base.rb @@ -0,0 +1,17 @@ +module Bitbucket + module Representation + class Base + def initialize(raw) + @raw = raw + end + + def self.decorate(entries) + entries.map { |entry| new(entry)} + end + + private + + attr_reader :raw + end + end +end diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb new file mode 100644 index 0000000000000000000000000000000000000000..4937aa9728f97fe1b3f7aba6d1a0f23b36576a2f --- /dev/null +++ b/lib/bitbucket/representation/comment.rb @@ -0,0 +1,27 @@ +module Bitbucket + module Representation + class Comment < Representation::Base + def author + user['username'] + end + + def note + raw.fetch('content', {}).fetch('raw', nil) + end + + def created_at + raw['created_on'] + end + + def updated_at + raw['updated_on'] || raw['created_on'] + end + + private + + def user + raw.fetch('user', {}) + end + end + end +end diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb new file mode 100644 index 0000000000000000000000000000000000000000..054064395c397102ce15535144e502a9c42055ce --- /dev/null +++ b/lib/bitbucket/representation/issue.rb @@ -0,0 +1,53 @@ +module Bitbucket + module Representation + class Issue < Representation::Base + CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze + + def iid + raw['id'] + end + + def kind + raw['kind'] + end + + def author + raw.fetch('reporter', {}).fetch('username', nil) + end + + def description + raw.fetch('content', {}).fetch('raw', nil) + end + + def state + closed? ? 'closed' : 'opened' + end + + def title + raw['title'] + end + + def milestone + raw['milestone']['name'] if raw['milestone'].present? + end + + def created_at + raw['created_on'] + end + + def updated_at + raw['edited_on'] + end + + def to_s + iid + end + + private + + def closed? + CLOSED_STATUS.include?(raw['state']) + end + end + end +end diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb new file mode 100644 index 0000000000000000000000000000000000000000..eebf8093380723de7a3078f4005a12a5b694dccc --- /dev/null +++ b/lib/bitbucket/representation/pull_request.rb @@ -0,0 +1,65 @@ +module Bitbucket + module Representation + class PullRequest < Representation::Base + def author + raw.fetch('author', {}).fetch('username', nil) + end + + def description + raw['description'] + end + + def iid + raw['id'] + end + + def state + if raw['state'] == 'MERGED' + 'merged' + elsif raw['state'] == 'DECLINED' + 'closed' + else + 'opened' + end + end + + def created_at + raw['created_on'] + end + + def updated_at + raw['updated_on'] + end + + def title + raw['title'] + end + + def source_branch_name + source_branch.fetch('branch', {}).fetch('name', nil) + end + + def source_branch_sha + source_branch.fetch('commit', {}).fetch('hash', nil) + end + + def target_branch_name + target_branch.fetch('branch', {}).fetch('name', nil) + end + + def target_branch_sha + target_branch.fetch('commit', {}).fetch('hash', nil) + end + + private + + def source_branch + raw['source'] + end + + def target_branch + raw['destination'] + end + end + end +end diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f8efe03bae9b671adb01d1edccba9b94aa67df8 --- /dev/null +++ b/lib/bitbucket/representation/pull_request_comment.rb @@ -0,0 +1,39 @@ +module Bitbucket + module Representation + class PullRequestComment < Comment + def iid + raw['id'] + end + + def file_path + inline.fetch('path') + end + + def old_pos + inline.fetch('from') + end + + def new_pos + inline.fetch('to') + end + + def parent_id + raw.fetch('parent', {}).fetch('id', nil) + end + + def inline? + raw.has_key?('inline') + end + + def has_parent? + raw.has_key?('parent') + end + + private + + def inline + raw.fetch('inline', {}) + end + end + end +end diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb new file mode 100644 index 0000000000000000000000000000000000000000..423eff8f2a5ae98fa9434d5193868b27337910ff --- /dev/null +++ b/lib/bitbucket/representation/repo.rb @@ -0,0 +1,71 @@ +module Bitbucket + module Representation + class Repo < Representation::Base + attr_reader :owner, :slug + + def initialize(raw) + super(raw) + end + + def owner_and_slug + @owner_and_slug ||= full_name.split('/', 2) + end + + def owner + owner_and_slug.first + end + + def slug + owner_and_slug.last + end + + def clone_url(token = nil) + url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href') + + if token.present? + clone_url = URI::parse(url) + clone_url.user = "x-token-auth:#{token}" + clone_url.to_s + else + url + end + end + + def description + raw['description'] + end + + def full_name + raw['full_name'] + end + + def issues_enabled? + raw['has_issues'] + end + + def name + raw['name'] + end + + def valid? + raw['scm'] == 'git' + end + + def has_wiki? + raw['has_wiki'] + end + + def visibility_level + if raw['is_private'] + Gitlab::VisibilityLevel::PRIVATE + else + Gitlab::VisibilityLevel::PUBLIC + end + end + + def to_s + full_name + end + end + end +end diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb new file mode 100644 index 0000000000000000000000000000000000000000..ba6b7667b49f8610f3932c3111b35d798219e805 --- /dev/null +++ b/lib/bitbucket/representation/user.rb @@ -0,0 +1,9 @@ +module Bitbucket + module Representation + class User < Representation::Base + def username + raw['username'] + end + end + end +end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index 229050151d379e824a51fbc031378ec18154d758..c10d3616f312cade8ab28d14be3341211544d21d 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -105,7 +105,7 @@ module Ci break elsif s.scan(/</) @out << '<' - elsif s.scan(/\n/) + elsif s.scan(/\r?\n/) @out << '<br>' else @out << s.scan(/./m) diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index a6b9beecded18128ccd4ee5d3b7217e05918edff..24bb3649a7673cd3b56a390462ea151299cc0c9a 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -8,6 +8,16 @@ module Ci rack_response({ 'message' => '404 Not found' }.to_json, 404) end + # Retain 405 error rather than a 500 error for Grape 0.15.0+. + # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes + rescue_from Grape::Exceptions::MethodNotAllowed do |e| + error! e.message, e.status, e.headers + end + + rescue_from Grape::Exceptions::Base do |e| + error! e.message, e.status, e.headers + end + rescue_from :all do |exception| handle_api_exception(exception) end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index de3e224bcee36be95616c15cd7c6516c117f87cb..c4bdef781f7070402e5c4f0769d6f04dd1868704 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -50,7 +50,7 @@ module Ci put ":id" do authenticate_runner! build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id]) - forbidden!('Build has been erased!') if build.erased? + validate_build!(build) update_runner_info @@ -80,9 +80,7 @@ module Ci # PATCH /builds/:id/trace.txt patch ":id/trace.txt" do build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) - forbidden!('Build has been erased!') if build.erased? + authenticate_build!(build) error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') content_range = request.headers['Content-Range'] @@ -113,8 +111,7 @@ module Ci Gitlab::Workhorse.verify_api_request!(headers) not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) forbidden!('build is not running') unless build.running? if params[:filesize] @@ -151,10 +148,8 @@ module Ci require_gitlab_workhorse! not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) forbidden!('Build is not running!') unless build.running? - forbidden!('Build has been erased!') if build.erased? artifacts_upload_path = ArtifactUploader.artifacts_upload_path artifacts = uploaded_file(:file, artifacts_upload_path) @@ -185,8 +180,7 @@ module Ci # GET /builds/:id/artifacts get ":id/artifacts" do build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) artifacts_file = build.artifacts_file unless artifacts_file.file_storage? @@ -211,8 +205,7 @@ module Ci # DELETE /builds/:id/artifacts delete ":id/artifacts" do build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) build.erase_artifacts! end diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index e608f5f6cade3394fa7c085d982f541f35626cd6..5ff25a3a9b24d534d9229bd1ea0c3c806cff8322 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -13,8 +13,19 @@ module Ci forbidden! unless current_runner end - def authenticate_build_token!(build) - forbidden! unless build_token_valid?(build) + def authenticate_build!(build) + validate_build!(build) do + forbidden! unless build_token_valid?(build) + end + end + + def validate_build!(build) + not_found! unless build + + yield if block_given? + + forbidden!('Project has been deleted!') unless build.project + forbidden!('Build has been erased!') if build.erased? end def runner_registration_token_valid? @@ -49,7 +60,7 @@ module Ci end def build_not_found! - if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) + if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) no_content! else not_found! diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index fef652cb975319a7324f2b2a0c797c96e5fda7f6..7463bd719d5bb14398d4f516d648ebde9b95593c 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -118,7 +118,7 @@ module Ci .merge(job_variables(name)) variables.map do |key, value| - { key: key, value: value, public: true } + { key: key.to_s, value: value, public: true } end end diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb index fb04a7824b82ef9a6efa180871e0b1d8ebc50aaa..63f9f8d7a5af7f071ab66a34f11c4d67c09550c1 100644 --- a/lib/email_template_interceptor.rb +++ b/lib/email_template_interceptor.rb @@ -5,8 +5,8 @@ class EmailTemplateInterceptor def self.delivering_email(message) # Remove HTML part if HTML emails are disabled. unless current_application_settings.html_emails_enabled - message.part.delete_if do |part| - part.content_type.try(:start_with?, 'text/html') + message.parts.delete_if do |part| + part.content_type.start_with?('text/html') end end end diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb new file mode 100644 index 0000000000000000000000000000000000000000..f48abcc86d53ac932898619b76c35a361dcffaa6 --- /dev/null +++ b/lib/gitlab/allowable.rb @@ -0,0 +1,7 @@ +module Gitlab + module Allowable + def can?(user, action, subject) + Ability.allowed?(user, action, subject) + end + end +end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 9667df4ffb8467b0029c97ee0dcdea4a90284662..0618107e2c3f6148ba4f3af455f1f2a00c28fb48 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,4 +1,6 @@ require 'asciidoctor' +require 'asciidoctor/converter/html5' +require "asciidoctor-plantuml" module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters @@ -23,16 +25,51 @@ module Gitlab def self.render(input, context, asciidoc_opts = {}) asciidoc_opts.reverse_merge!( safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: [] ) asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) + plantuml_setup + html = ::Asciidoctor.convert(input, asciidoc_opts) html = Banzai.post_process(html, context) html.html_safe end + + def self.plantuml_setup + Asciidoctor::PlantUml.configure do |conf| + conf.url = ApplicationSetting.current.plantuml_url + conf.svg_enable = ApplicationSetting.current.plantuml_enabled + conf.png_enable = ApplicationSetting.current.plantuml_enabled + conf.txt_enable = false + end + end + + class Html5Converter < Asciidoctor::Converter::Html5Converter + extend Asciidoctor::Converter::Config + + register_for 'gitlab_html5' + + def stem(node) + return super unless node.style.to_sym == :latexmath + + %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>) + end + + def inline_quoted(node) + return super unless node.type.to_sym == :latexmath + + %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>) + end + + private + + def id_attribute(node) + node.id ? %( id="#{node.id}") : nil + end + end end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index aca5d0020cf5874313bcbac619dee3501ae77136..8dda65c71ef61644b1b1127598f81da5642b693b 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,6 +2,10 @@ module Gitlab module Auth class MissingPersonalTokenError < StandardError; end + SCOPES = [:api, :read_user] + DEFAULT_SCOPES = [:api] + OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES + class << self def find_for_git_client(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? @@ -88,7 +92,7 @@ module Gitlab def oauth_access_token_check(login, password) if login == "oauth2" && password.present? token = Doorkeeper::AccessToken.by_token(password) - if token && token.accessible? + if valid_oauth_token?(token) user = User.find_by(id: token.resource_owner_id) Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities) end @@ -97,12 +101,27 @@ module Gitlab def personal_access_token_check(login, password) if login && password - user = User.find_by_personal_access_token(password) + token = PersonalAccessToken.active.find_by_token(password) validation = User.by_login(login) - Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation + + if valid_personal_access_token?(token, validation) + Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities) + end end end + def valid_oauth_token?(token) + token && token.accessible? && valid_api_token?(token) + end + + def valid_personal_access_token?(token, user) + token && token.user == user && valid_api_token?(token) + end + + def valid_api_token?(token) + AccessTokenValidationService.new(token).include_any_scope?(['api']) + end + def lfs_token_check(login, password) deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/) diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb index 6be7f690676b0169dfb1f27e5c12032d5ece35bc..39b86c61a18f30da55fcc72fc9b0f7a4b68fcaad 100644 --- a/lib/gitlab/auth/result.rb +++ b/lib/gitlab/auth/result.rb @@ -9,8 +9,7 @@ module Gitlab def lfs_deploy_token?(for_project) type == :lfs_deploy_token && - actor && - actor.projects.include?(for_project) + actor.try(:has_access_to?, for_project) end def success? diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb index 50aa45e540674456729a3c66977a446312d74e1b..b762d85b6e5378b56a1693bc03c0f229f2ec7b7e 100644 --- a/lib/gitlab/badge/build/status.rb +++ b/lib/gitlab/badge/build/status.rb @@ -20,8 +20,8 @@ module Gitlab def status @project.pipelines - .where(sha: @sha, ref: @ref) - .status || 'unknown' + .where(sha: @sha) + .latest_status(@ref) || 'unknown' end def metadata diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb deleted file mode 100644 index 7298152e7e98515524815f239706ca9abf9f558d..0000000000000000000000000000000000000000 --- a/lib/gitlab/bitbucket_import.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Gitlab - module BitbucketImport - mattr_accessor :public_key - @public_key = nil - end -end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb deleted file mode 100644 index 8d1ad62fae0b088e51944a88ea45adc86a5c8844..0000000000000000000000000000000000000000 --- a/lib/gitlab/bitbucket_import/client.rb +++ /dev/null @@ -1,142 +0,0 @@ -module Gitlab - module BitbucketImport - class Client - class Unauthorized < StandardError; end - - attr_reader :consumer, :api - - def self.from_project(project) - import_data_credentials = project.import_data.credentials if project.import_data - if import_data_credentials && import_data_credentials[:bb_session] - token = import_data_credentials[:bb_session][:bitbucket_access_token] - token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret] - new(token, token_secret) - else - raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}" - end - end - - def initialize(access_token = nil, access_token_secret = nil) - @consumer = ::OAuth::Consumer.new( - config.app_id, - config.app_secret, - bitbucket_options - ) - - if access_token && access_token_secret - @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret) - end - end - - def request_token(redirect_uri) - request_token = consumer.get_request_token(oauth_callback: redirect_uri) - - { - oauth_token: request_token.token, - oauth_token_secret: request_token.secret, - oauth_callback_confirmed: request_token.callback_confirmed?.to_s - } - end - - def authorize_url(request_token, redirect_uri) - request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) - - if request_token.callback_confirmed? - request_token.authorize_url - else - request_token.authorize_url(oauth_callback: redirect_uri) - end - end - - def get_token(request_token, oauth_verifier, redirect_uri) - request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) - - if request_token.callback_confirmed? - request_token.get_access_token(oauth_verifier: oauth_verifier) - else - request_token.get_access_token(oauth_callback: redirect_uri) - end - end - - def user - JSON.parse(get("/api/1.0/user").body) - end - - def issues(project_identifier) - all_issues = [] - offset = 0 - per_page = 50 # Maximum number allowed by Bitbucket - index = 0 - - begin - issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body) - # Find out how many total issues are present - total = issues["count"] if index == 0 - all_issues.concat(issues["issues"]) - offset += issues["issues"].count - index += 1 - end while all_issues.count < total - - all_issues - end - - def issue_comments(project_identifier, issue_id) - comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) - comments.sort_by { |comment| comment["utc_created_on"] } - end - - def project(project_identifier) - JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body) - end - - def find_deploy_key(project_identifier, key) - JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| - deploy_key["key"].chomp == key.chomp - end - end - - def add_deploy_key(project_identifier, key) - deploy_key = find_deploy_key(project_identifier, key) - return if deploy_key - - JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body) - end - - def delete_deploy_key(project_identifier, key) - deploy_key = find_deploy_key(project_identifier, key) - return unless deploy_key - - api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204" - end - - def projects - JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } - end - - def incompatible_projects - JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" } - end - - private - - def get(url) - response = api.get(url) - raise Unauthorized if (400..499).cover?(response.code.to_i) - - response - end - - def issue_api_endpoint(project_identifier, per_page, offset) - "/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}" - end - - def config - Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" } - end - - def bitbucket_options - OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys - end - end - end -end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index f4b5097adb1f4c18d915ed781378c3ff157d22f3..44323b47dca856e65a06e7fa1ea690bc45f6ffaf 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -1,84 +1,247 @@ module Gitlab module BitbucketImport class Importer - attr_reader :project, :client + include Gitlab::ShellAdapter + + LABELS = [{ title: 'bug', color: '#FF0000' }, + { title: 'enhancement', color: '#428BCA' }, + { title: 'proposal', color: '#69D100' }, + { title: 'task', color: '#7F8C8D' }].freeze + + attr_reader :project, :client, :errors, :users def initialize(project) @project = project - @client = Client.from_project(@project) + @client = Bitbucket::Client.new(project.import_data.credentials) @formatter = Gitlab::ImportFormatter.new + @labels = {} + @errors = [] + @users = {} end def execute - import_issues if has_issues? + import_wiki + import_issues + import_pull_requests + handle_errors true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error.new, e.message - ensure - Gitlab::BitbucketImport::KeyDeleter.new(project).execute end private - def gitlab_user_id(project, bitbucket_id) - if bitbucket_id - user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) - (user && user.id) || project.creator_id - else - project.creator_id - end + def handle_errors + return unless errors.any? + + project.update_column(:import_error, { + message: 'The remote data could not be fully imported.', + errors: errors + }.to_json) + end + + def gitlab_user_id(project, username) + find_user_id(username) || project.creator_id end - def identifier - project.import_source + def find_user_id(username) + return nil unless username + + return users[username] if users.key?(username) + + users[username] = User.select(:id) + .joins(:identities) + .find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username) + .try(:id) end - def has_issues? - client.project(identifier)["has_issues"] + def repo + @repo ||= client.repo(project.import_source) end - def import_issues - issues = client.issues(identifier) + def import_wiki + return if project.wiki.repository_exists? - issues.each do |issue| - body = '' - reporter = nil - author = 'Anonymous' + path_with_namespace = "#{project.path_with_namespace}.wiki" + import_url = project.import_url.sub(/\.git\z/, ".git/wiki") + gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url) + rescue StandardError => e + errors << { type: :wiki, errors: e.message } + end - if issue["reported_by"] && issue["reported_by"]["username"] - reporter = issue["reported_by"]["username"] - author = reporter + def import_issues + return unless repo.issues_enabled? + + create_labels + + client.issues(repo).each do |issue| + begin + description = '' + description += @formatter.author_line(issue.author) unless find_user_id(issue.author) + description += issue.description + + label_name = issue.kind + milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil + + gitlab_issue = project.issues.create!( + iid: issue.iid, + title: issue.title, + description: description, + state: issue.state, + author_id: gitlab_user_id(project, issue.author), + milestone: milestone, + created_at: issue.created_at, + updated_at: issue.updated_at + ) + + gitlab_issue.labels << @labels[label_name] + + import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted? + rescue StandardError => e + errors << { type: :issue, iid: issue.iid, errors: e.message } end + end + end - body = @formatter.author_line(author) - body += issue["content"] + def import_issue_comments(issue, gitlab_issue) + client.issue_comments(repo, issue.iid).each do |comment| + # The note can be blank for issue service messages like "Changed title: ..." + # We would like to import those comments as well but there is no any + # specific parameter that would allow to process them, it's just an empty comment. + # To prevent our importer from just crashing or from creating useless empty comments + # we do this check. + next unless comment.note.present? + + note = '' + note += @formatter.author_line(comment.author) unless find_user_id(comment.author) + note += comment.note + + begin + gitlab_issue.notes.create!( + project: project, + note: note, + author_id: gitlab_user_id(project, comment.author), + created_at: comment.created_at, + updated_at: comment.updated_at + ) + rescue StandardError => e + errors << { type: :issue_comment, iid: issue.iid, errors: e.message } + end + end + end - comments = client.issue_comments(identifier, issue["local_id"]) + def create_labels + LABELS.each do |label| + @labels[label[:title]] = project.labels.create!(label) + end + end - if comments.any? - body += @formatter.comments_header + def import_pull_requests + pull_requests = client.pull_requests(repo) + + pull_requests.each do |pull_request| + begin + description = '' + description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author) + description += pull_request.description + + merge_request = project.merge_requests.create( + iid: pull_request.iid, + title: pull_request.title, + description: description, + source_project: project, + source_branch: pull_request.source_branch_name, + source_branch_sha: pull_request.source_branch_sha, + target_project: project, + target_branch: pull_request.target_branch_name, + target_branch_sha: pull_request.target_branch_sha, + state: pull_request.state, + author_id: gitlab_user_id(project, pull_request.author), + assignee_id: nil, + created_at: pull_request.created_at, + updated_at: pull_request.updated_at + ) + + import_pull_request_comments(pull_request, merge_request) if merge_request.persisted? + rescue StandardError => e + errors << { type: :pull_request, iid: pull_request.iid, errors: e.message } end + end + end + + def import_pull_request_comments(pull_request, merge_request) + comments = client.pull_request_comments(repo, pull_request.iid) + + inline_comments, pr_comments = comments.partition(&:inline?) + + import_inline_comments(inline_comments, pull_request, merge_request) + import_standalone_pr_comments(pr_comments, merge_request) + end - comments.each do |comment| - author = 'Anonymous' + def import_inline_comments(inline_comments, pull_request, merge_request) + line_code_map = {} - if comment["author_info"] && comment["author_info"]["username"] - author = comment["author_info"]["username"] - end + children, parents = inline_comments.partition(&:has_parent?) - body += @formatter.comment(author, comment["utc_created_on"], comment["content"]) + # The Bitbucket API returns threaded replies as parent-child + # relationships. We assume that the child can appear in any order in + # the JSON. + parents.each do |comment| + line_code_map[comment.iid] = generate_line_code(comment) + end + + children.each do |comment| + line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil) + end + + inline_comments.each do |comment| + begin + attributes = pull_request_comment_attributes(comment) + attributes.merge!( + position: build_position(merge_request, comment), + line_code: line_code_map.fetch(comment.iid), + type: 'DiffNote') + + merge_request.notes.create!(attributes) + rescue StandardError => e + errors << { type: :pull_request, iid: comment.iid, errors: e.message } end + end + end + + def build_position(merge_request, pr_comment) + params = { + diff_refs: merge_request.diff_refs, + old_path: pr_comment.file_path, + new_path: pr_comment.file_path, + old_line: pr_comment.old_pos, + new_line: pr_comment.new_pos + } - project.issues.create!( - description: body, - title: issue["title"], - state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', - author_id: gitlab_user_id(project, reporter) - ) + Gitlab::Diff::Position.new(params) + end + + def import_standalone_pr_comments(pr_comments, merge_request) + pr_comments.each do |comment| + begin + merge_request.notes.create!(pull_request_comment_attributes(comment)) + rescue StandardError => e + errors << { type: :pull_request, iid: comment.iid, errors: e.message } + end end - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message + end + + def generate_line_code(pr_comment) + Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos) + end + + def pull_request_comment_attributes(comment) + { + project: project, + note: comment.note, + author_id: gitlab_user_id(project, comment.author), + created_at: comment.created_at, + updated_at: comment.updated_at + } end end end diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb deleted file mode 100644 index 0b63f025d0ad45900d8c4816e4703ab9200935c5..0000000000000000000000000000000000000000 --- a/lib/gitlab/bitbucket_import/key_adder.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Gitlab - module BitbucketImport - class KeyAdder - attr_reader :repo, :current_user, :client - - def initialize(repo, current_user, access_params) - @repo, @current_user = repo, current_user - @client = Client.new(access_params[:bitbucket_access_token], - access_params[:bitbucket_access_token_secret]) - end - - def execute - return false unless BitbucketImport.public_key.present? - - project_identifier = "#{repo["owner"]}/#{repo["slug"]}" - client.add_deploy_key(project_identifier, BitbucketImport.public_key) - - true - rescue - false - end - end - end -end diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb deleted file mode 100644 index e03c3155b3ebeebf18e9e80fe62d135596fc2e88..0000000000000000000000000000000000000000 --- a/lib/gitlab/bitbucket_import/key_deleter.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab - module BitbucketImport - class KeyDeleter - attr_reader :project, :current_user, :client - - def initialize(project) - @project = project - @current_user = project.creator - @client = Client.from_project(@project) - end - - def execute - return false unless BitbucketImport.public_key.present? - - client.delete_deploy_key(project.import_source, BitbucketImport.public_key) - - true - rescue - false - end - end - end -end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index b90ef0b0fba3e942bfbdeaf3951284d7676d509c..d94f70fd1fb69b5494639e79d261e90889b1f6aa 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -1,10 +1,11 @@ module Gitlab module BitbucketImport class ProjectCreator - attr_reader :repo, :namespace, :current_user, :session_data + attr_reader :repo, :name, :namespace, :current_user, :session_data - def initialize(repo, namespace, current_user, session_data) + def initialize(repo, name, namespace, current_user, session_data) @repo = repo + @name = name @namespace = namespace @current_user = current_user @session_data = session_data @@ -13,17 +14,24 @@ module Gitlab def execute ::Projects::CreateService.new( current_user, - name: repo["name"], - path: repo["slug"], - description: repo["description"], + name: name, + path: name, + description: repo.description, namespace_id: namespace.id, - visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, - import_type: "bitbucket", - import_source: "#{repo["owner"]}/#{repo["slug"]}", - import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", - import_data: { credentials: { bb_session: session_data } } + visibility_level: repo.visibility_level, + import_type: 'bitbucket', + import_source: repo.full_name, + import_url: repo.clone_url(session_data[:token]), + import_data: { credentials: session_data }, + skip_wiki: skip_wiki ).execute end + + private + + def skip_wiki + repo.has_wiki? + end end end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 25da8474e95a0f7c43c507b3f18be238ef849c63..4fe53ce93a99791d3be0a9e8fea635e81db5c451 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,6 +42,10 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end + + def presenter + Gitlab::ChatCommands::Presenter.new + end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index b0d3fdbc48ae321aa3981ee0d568ec0401ed2ae0..145086755e4a2240ef123b9097fecd88d75fdc6a 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -22,8 +22,6 @@ module Gitlab end end - private - def match_command match = nil service = available_commands.find do |klass| @@ -33,6 +31,8 @@ module Gitlab [service, match] end + private + def help_messages available_commands.map(&:help_message) end @@ -48,15 +48,15 @@ module Gitlab end def help(messages) - Mattermost::Presenter.help(messages, params[:command]) + presenter.help(messages, params[:command]) end def access_denied - Mattermost::Presenter.access_denied + presenter.access_denied end def present(resource) - Mattermost::Presenter.present(resource) + presenter.present(resource) end end end diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 0eed1fce0dc17fca0c851b773c27eea679f44ffd..7127d2f6d047cac40f779fb609c3e20521b417d2 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -4,7 +4,7 @@ module Gitlab include Gitlab::Routing.url_helpers def self.match(text) - /\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text) + /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) end def self.help_message @@ -50,7 +50,8 @@ module Gitlab def url(subject) polymorphic_url( - [ subject.project.namespace.becomes(Namespace), subject.project, subject ]) + [subject.project.namespace.becomes(Namespace), subject.project, subject] + ) end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 1dba85c1b513959444b4fd115d422e772a932790..cefb6775db854becff484cc09f75073489a1611f 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -8,7 +8,7 @@ module Gitlab end def self.help_message - 'issue new <title>\n<description>' + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' end def self.allowed?(project, user) diff --git a/lib/mattermost/presenter.rb b/lib/gitlab/chat_commands/presenter.rb similarity index 90% rename from lib/mattermost/presenter.rb rename to lib/gitlab/chat_commands/presenter.rb index 67eda983a74fd8638cdb4bd33eac07678fb1480b..8930a21f4065b231029d2ee986a2676211413c60 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -1,7 +1,7 @@ -module Mattermost - class Presenter - class << self - include Gitlab::Routing.url_helpers +module Gitlab + module ChatCommands + class Presenter + include Gitlab::Routing def authorize_chat_name(url) message = if url @@ -30,12 +30,12 @@ module Mattermost if subject.is_a?(Gitlab::ChatCommands::Result) show_result(subject) elsif subject.respond_to?(:count) - if subject.many? - multiple_resources(subject) - elsif subject.none? + if subject.none? not_found + elsif subject.one? + single_resource(subject.first) else - single_resource(subject) + multiple_resources(subject) end else single_resource(subject) @@ -64,16 +64,16 @@ module Mattermost def single_resource(resource) return error(resource) if resource.errors.any? || !resource.persisted? - message = "### #{title(resource)}" + message = "#{title(resource)}:" message << "\n\n#{resource.description}" if resource.try(:description) in_channel_response(message) end def multiple_resources(resources) - resources.map! { |resource| title(resource) } + titles = resources.map { |resource| title(resource) } - message = header_with_list("Multiple results were found:", resources) + message = header_with_list("Multiple results were found:", titles) ephemeral_response(message) end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index cb1065223d4d65b9de3602a5dbc8df63a977137f..273118135a90a142664309c4176430e5a5968ebe 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -1,13 +1,16 @@ module Gitlab module Checks class ChangeAccess - attr_reader :user_access, :project + attr_reader :user_access, :project, :skip_authorization - def initialize(change, user_access:, project:) + def initialize( + change, user_access:, project:, env: {}, skip_authorization: false) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @branch_name = Gitlab::Git.branch_name(@ref) @user_access = user_access @project = project + @env = env + @skip_authorization = skip_authorization end def exec @@ -23,12 +26,13 @@ module Gitlab protected def protected_branch_checks + return if skip_authorization return unless @branch_name return unless project.protected_branch?(@branch_name) - if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches) + if forced_push? return "You are not allowed to force push code to a protected branch on this project." - elsif Gitlab::Git.blank_ref?(@newrev) && user_access.cannot_do_action?(:remove_protected_branches) + elsif Gitlab::Git.blank_ref?(@newrev) return "You are not allowed to delete protected branches from this project." end @@ -48,6 +52,8 @@ module Gitlab end def tag_checks + return if skip_authorization + tag_ref = Gitlab::Git.tag_name(@ref) if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project) @@ -56,6 +62,8 @@ module Gitlab end def push_checks + return if skip_authorization + if user_access.cannot_do_action?(:push_code) "You are not allowed to push code to this project." end @@ -68,7 +76,7 @@ module Gitlab end def forced_push? - Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev) + Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env) end def matching_merge_request? diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb index 5fe86553bd014486944dc6fa936457e490e93628..de0c9049ebf346f9b7a4e79e2603d10b33c8005c 100644 --- a/lib/gitlab/checks/force_push.rb +++ b/lib/gitlab/checks/force_push.rb @@ -1,15 +1,20 @@ module Gitlab module Checks class ForcePush - def self.force_push?(project, oldrev, newrev) + def self.force_push?(project, oldrev, newrev, env: {}) return false if project.empty_repo? # Created or deleted branch if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) false else - missed_ref, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list --max-count=1 #{oldrev} ^#{newrev})) - missed_ref.present? + missed_ref, exit_status = Gitlab::Git::RevList.new(oldrev, newrev, project: project, env: env).execute + + if exit_status == 0 + missed_ref.present? + else + raise "Got a non-zero exit code while calling out to `git rev-list` in the force-push check." + end end end end diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index b7b4b91eb516deabe209d2b9d7ea0e3f1d2926ab..f7c530c7d9fa584275c4ae827ccd22feddd5997a 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -33,7 +33,6 @@ module Gitlab validates :url, length: { maximum: 255 }, - addressable_url: true, allow_nil: true validates :action, diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb new file mode 100644 index 0000000000000000000000000000000000000000..a979fe7d573939eee5fcba65c199ea295b40276e --- /dev/null +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -0,0 +1,37 @@ +module Gitlab + module Ci + module Status + module Build + class Cancelable < SimpleDelegator + include Status::Extended + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'ban' + end + + def action_path + cancel_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def action_title + 'Cancel' + end + + def self.matches?(build, user) + build.cancelable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb new file mode 100644 index 0000000000000000000000000000000000000000..3fec2c5d4dbeada7cc9967eb3d387075b8837648 --- /dev/null +++ b/lib/gitlab/ci/status/build/common.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + module Build + module Common + def has_details? + can?(user, :read_build, subject) + end + + def details_path + namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb new file mode 100644 index 0000000000000000000000000000000000000000..eee9a64120b287ad29724d425d1432717e75074b --- /dev/null +++ b/lib/gitlab/ci/status/build/factory.rb @@ -0,0 +1,18 @@ +module Gitlab + module Ci + module Status + module Build + class Factory < Status::Factory + def self.extended_statuses + [Status::Build::Stop, Status::Build::Play, + Status::Build::Cancelable, Status::Build::Retryable] + end + + def self.common_helpers + Status::Build::Common + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb new file mode 100644 index 0000000000000000000000000000000000000000..1bf949c96dd9db9ae9d2dd1644089ac74b0f1265 --- /dev/null +++ b/lib/gitlab/ci/status/build/play.rb @@ -0,0 +1,57 @@ +module Gitlab + module Ci + module Status + module Build + class Play < SimpleDelegator + include Status::Extended + + def text + 'manual' + end + + def label + 'manual play action' + end + + def icon + 'icon_status_manual' + end + + def group + 'manual' + end + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'play' + end + + def action_title + 'Play' + end + + def action_class + 'ci-play-icon' + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def self.matches?(build, user) + build.playable? && !build.stops_environment? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb new file mode 100644 index 0000000000000000000000000000000000000000..8e38d6a8523c19e506fea4537f3db2d011cde202 --- /dev/null +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -0,0 +1,37 @@ +module Gitlab + module Ci + module Status + module Build + class Retryable < SimpleDelegator + include Status::Extended + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'refresh' + end + + def action_title + 'Retry' + end + + def action_path + retry_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def self.matches?(build, user) + build.retryable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb new file mode 100644 index 0000000000000000000000000000000000000000..e1dfdb76d414777a7575140b2b7b2fb1c475c518 --- /dev/null +++ b/lib/gitlab/ci/status/build/stop.rb @@ -0,0 +1,53 @@ +module Gitlab + module Ci + module Status + module Build + class Stop < SimpleDelegator + include Status::Extended + + def text + 'manual' + end + + def label + 'manual stop action' + end + + def icon + 'icon_status_manual' + end + + def group + 'manual' + end + + def has_action? + can?(user, :update_build, subject) + end + + def action_icon + 'stop' + end + + def action_title + 'Stop' + end + + def action_path + play_namespace_project_build_path(subject.project.namespace, + subject.project, + subject) + end + + def action_method + :post + end + + def self.matches?(build, user) + build.playable? && build.stops_environment? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index ce4108fdcf2ff8debdcc01e7b98c81d967d5b0b6..73b6ab5a635e305eb830f506a0df9e857eebedf0 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -4,10 +4,14 @@ module Gitlab # Base abstract class fore core status # class Core - include Gitlab::Routing.url_helpers + include Gitlab::Routing + include Gitlab::Allowable - def initialize(subject) + attr_reader :subject, :user + + def initialize(subject, user) @subject = subject + @user = user end def icon @@ -18,23 +22,12 @@ module Gitlab raise NotImplementedError end - def title - "#{@subject.class.name.demodulize}: #{label}" - end - - # Deprecation warning: this method is here because we need to maintain - # backwards compatibility with legacy statuses. We often do something - # like "ci-status ci-status-#{status}" to set CSS class. - # - # `to_s` method should be renamed to `group` at some point, after - # phasing legacy satuses out. - # - def to_s - self.class.name.demodulize.downcase.underscore + def group + self.class.name.demodulize.underscore end def has_details? - raise NotImplementedError + false end def details_path @@ -42,16 +35,27 @@ module Gitlab end def has_action? - raise NotImplementedError + false end def action_icon raise NotImplementedError end + def action_class + end + def action_path raise NotImplementedError end + + def action_method + raise NotImplementedError + end + + def action_title + raise NotImplementedError + end end end end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb index 6bfb5d38c1f1c1bdfffe322384e6f2da2b9d8b20..d367c9bda69ff2d71f6e6cef91c3ef66fbc7b2b2 100644 --- a/lib/gitlab/ci/status/extended.rb +++ b/lib/gitlab/ci/status/extended.rb @@ -2,8 +2,12 @@ module Gitlab module Ci module Status module Extended - def matches?(_subject) - raise NotImplementedError + extend ActiveSupport::Concern + + class_methods do + def matches?(_subject, _user) + raise NotImplementedError + end end end end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index b2f896f2211fc8a597d9edc470c793e806cdd3e8..ae9ef895df49d27e3bb9c81ac02df667fee58e1d 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -2,10 +2,9 @@ module Gitlab module Ci module Status class Factory - attr_reader :subject - - def initialize(subject) + def initialize(subject, user) @subject = subject + @user = user end def fabricate! @@ -16,27 +15,32 @@ module Gitlab end end + def self.extended_statuses + [] + end + + def self.common_helpers + Module.new + end + private - def subject_status - @subject_status ||= subject.status + def simple_status + @simple_status ||= @subject.status || :created end def core_status Gitlab::Ci::Status - .const_get(subject_status.capitalize) - .new(subject) + .const_get(simple_status.capitalize) + .new(@subject, @user) + .extend(self.class.common_helpers) end def extended_status - @extended ||= extended_statuses.find do |status| - status.matches?(subject) + @extended ||= self.class.extended_statuses.find do |status| + status.matches?(@subject, @user) end end - - def extended_statuses - [] - end end end end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb index 25e52bec3da1db99432dd097a4def7d30931d324..76bfd18bf4040f369e53a751d22d8f66d92eb337 100644 --- a/lib/gitlab/ci/status/pipeline/common.rb +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -4,13 +4,13 @@ module Gitlab module Pipeline module Common def has_details? - true + can?(user, :read_pipeline, subject) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject) end def has_action? diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 4ac4ec671d0eb7fdcc84322e2417148c3ff83046..16dcb326be9c46503d0fcd6a19ee6fc1057159b0 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -3,14 +3,12 @@ module Gitlab module Status module Pipeline class Factory < Status::Factory - private - - def extended_statuses + def self.extended_statuses [Pipeline::SuccessWithWarnings] end - def core_status - super.extend(Status::Pipeline::Common) + def self.common_helpers + Status::Pipeline::Common end end end diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb index 4b040d60df83320a8e427b04e4736e16cba38821..24bf8b869e04cb45900ace17f9e42402dac14792 100644 --- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -3,7 +3,7 @@ module Gitlab module Status module Pipeline class SuccessWithWarnings < SimpleDelegator - extend Status::Extended + include Status::Extended def text 'passed' @@ -17,11 +17,11 @@ module Gitlab 'icon_status_warning' end - def to_s + def group 'success_with_warnings' end - def self.matches?(pipeline) + def self.matches?(pipeline, user) pipeline.success? && pipeline.has_warnings? end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index 14c437d2b98db80214f48dc123acee8ebb41614e..7852f492e1d0738b8f2e33af2e7412dd0c2e8356 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -4,14 +4,14 @@ module Gitlab module Stage module Common def has_details? - true + can?(user, :read_pipeline, subject.pipeline) end def details_path - namespace_project_pipeline_path(@subject.project.namespace, - @subject.project, - @subject.pipeline, - anchor: @subject.name) + namespace_project_pipeline_path(subject.project.namespace, + subject.project, + subject.pipeline, + anchor: subject.name) end def has_action? diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index c6522d5ada154485fab858027db84ece8d453a9c..689a5dd45bc3911e9d323c0fde6c02ec60a4afce 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -3,10 +3,8 @@ module Gitlab module Status module Stage class Factory < Status::Factory - private - - def core_status - super.extend(Status::Stage::Common) + def self.common_helpers + Status::Stage::Common end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index c6bb8f9c8ed126a9cbb916a896a2a27dbb2edf2d..2ff27e46d646766a129e0dea025ecb4a18b3c7e2 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -35,6 +35,7 @@ module Gitlab signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], koding_enabled: false, + plantuml_enabled: false, sign_in_text: nil, after_sign_up_text: nil, help_page_text: nil, @@ -45,7 +46,7 @@ module Gitlab default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], domain_whitelist: Settings.gitlab['domain_whitelist'], - import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], + import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb similarity index 59% rename from lib/gitlab/cycle_analytics/base_event.rb rename to lib/gitlab/cycle_analytics/base_event_fetcher.rb index 53a148ad703504cb70c19bd2894dc6974ca09761..0d8791d396b5aaac9e0085c1675571cc012d0621 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -1,13 +1,13 @@ module Gitlab module CycleAnalytics - class BaseEvent - include MetricsTables + class BaseEventFetcher + include BaseQuery - attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query + attr_reader :projections, :query, :stage, :order - def initialize(project:, options:) - @query = EventsQuery.new(project: project, options: options) + def initialize(project:, stage:, options:) @project = project + @stage = stage @options = options end @@ -19,10 +19,8 @@ module Gitlab end.compact end - def custom_query(_base_query); end - def order - @order || @start_time_attrs + @order || default_order end private @@ -34,7 +32,17 @@ module Gitlab end def event_result - @event_result ||= @query.execute(self).to_a + @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a + end + + def events_query + diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs]) + + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc) + end + + def default_order + [@options[:start_time_attrs]].flatten.first end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb new file mode 100644 index 0000000000000000000000000000000000000000..d560dca45c8abbec50ecf5790b9fa0d9715e01e8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -0,0 +1,31 @@ +module Gitlab + module CycleAnalytics + module BaseQuery + include MetricsTables + include Gitlab::Database::Median + include Gitlab::Database::DateTime + + private + + def base_query + @base_query ||= stage_query + end + + def stage_query + query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). + join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). + where(issue_table[:project_id].eq(@project.id)). + where(issue_table[:deleted_at].eq(nil)). + where(issue_table[:created_at].gteq(@options[:from])) + + # Load merge_requests + query = query.join(mr_table, Arel::Nodes::OuterJoin). + on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). + join(mr_metrics_table). + on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) + + query + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..74bbcdcb3dd1ff9255dce4754d9bc4cdba671b1a --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -0,0 +1,54 @@ +module Gitlab + module CycleAnalytics + class BaseStage + include BaseQuery + + def initialize(project:, options:) + @project = project + @options = options + end + + def events + event_fetcher.fetch + end + + def as_json + AnalyticsStageSerializer.new.represent(self).as_json + end + + def title + name.to_s.capitalize + end + + def median + cte_table = Arel::Table.new("cte_table_for_#{name}") + + # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). + # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). + # We compute the (end_time - start_time) interval, and give it an alias based on the current + # cycle analytics stage. + interval_query = Arel::Nodes::As.new( + cte_table, + subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s)) + + median_datetime(cte_table, interval_query, name) + end + + def name + raise NotImplementedError.new("Expected #{self.name} to implement name") + end + + private + + def event_fetcher + @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project, + stage: name, + options: event_options) + end + + def event_options + @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb similarity index 76% rename from lib/gitlab/cycle_analytics/review_event.rb rename to lib/gitlab/cycle_analytics/code_event_fetcher.rb index b394a02cc529f173f578e2d66490b91f649b6c0e..5245b9ca8fc3cee18148710420a6bb890e48619e 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -1,22 +1,22 @@ module Gitlab module CycleAnalytics - class ReviewEvent < BaseEvent + class CodeEventFetcher < BaseEventFetcher include MergeRequestAllowed def initialize(*args) - @stage = :review - @start_time_attrs = mr_table[:created_at] - @end_time_attrs = mr_metrics_table[:merged_at] @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], mr_table[:created_at], mr_table[:state], mr_table[:author_id]] + @order = mr_table[:created_at] super(*args) end + private + def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..d1bc2055ba8cf78b5a81e1792b19d9315f17dcbb --- /dev/null +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + class CodeStage < BaseStage + def start_time_attrs + @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_table[:created_at] + end + + def name + :code + end + + def description + "Time until first merge request" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb new file mode 100644 index 0000000000000000000000000000000000000000..50e126cf00b02511ba4aaf8e0c070c7d40621f67 --- /dev/null +++ b/lib/gitlab/cycle_analytics/event_fetcher.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module EventFetcher + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb deleted file mode 100644 index 2d703d76cbb9be673b93250b044362300c0b7f26..0000000000000000000000000000000000000000 --- a/lib/gitlab/cycle_analytics/events.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Gitlab - module CycleAnalytics - class Events - def initialize(project:, options:) - @project = project - @options = options - end - - def issue_events - IssueEvent.new(project: @project, options: @options).fetch - end - - def plan_events - PlanEvent.new(project: @project, options: @options).fetch - end - - def code_events - CodeEvent.new(project: @project, options: @options).fetch - end - - def test_events - TestEvent.new(project: @project, options: @options).fetch - end - - def review_events - ReviewEvent.new(project: @project, options: @options).fetch - end - - def staging_events - StagingEvent.new(project: @project, options: @options).fetch - end - - def production_events - ProductionEvent.new(project: @project, options: @options).fetch - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb deleted file mode 100644 index 2418832ccc2fe02dc93a6acd71bdc13f6e100b46..0000000000000000000000000000000000000000 --- a/lib/gitlab/cycle_analytics/events_query.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Gitlab - module CycleAnalytics - class EventsQuery - attr_reader :project - - def initialize(project:, options: {}) - @project = project - @from = options[:from] - @branch = options[:branch] - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch) - end - - def execute(stage_class) - @stage_class = stage_class - - ActiveRecord::Base.connection.exec_query(query.to_sql) - end - - private - - def query - base_query = @fetcher.base_query_for(@stage_class.stage) - diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs) - - @stage_class.custom_query(base_query) - - base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc) - end - - def extract_epoch(arel_attribute) - return arel_attribute unless Gitlab::Database.postgresql? - - Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))}) - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb deleted file mode 100644 index 705b7e5ce24871addc58d903a45f25b52b878a4f..0000000000000000000000000000000000000000 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Gitlab - module CycleAnalytics - class IssueEvent < BaseEvent - include IssueAllowed - - def initialize(*args) - @stage = :issue - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] - - super(*args) - end - - private - - def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb similarity index 72% rename from lib/gitlab/cycle_analytics/production_event.rb rename to lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 4868c3c62373d942bea2d10bade2160cfbf1969e..0d8da99455eff72f75db8edd9dcbcd4cc808599f 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -1,12 +1,9 @@ module Gitlab module CycleAnalytics - class ProductionEvent < BaseEvent + class IssueEventFetcher < BaseEventFetcher include IssueAllowed def initialize(*args) - @stage = :production - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [issue_table[:title], issue_table[:iid], issue_table[:id], diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..d2068fbc38fe5718385592c00236939fcc163b6d --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class IssueStage < BaseStage + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end + + def name + :issue + end + + def description + "Time before an issue gets scheduled" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb deleted file mode 100644 index b71e8735e27e91cc7c660f1f1b76752d35a9b1ba..0000000000000000000000000000000000000000 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Gitlab - module CycleAnalytics - class MetricsFetcher - include Gitlab::Database::Median - include Gitlab::Database::DateTime - include MetricsTables - - DEPLOYMENT_METRIC_STAGES = %i[production staging] - - def initialize(project:, from:, branch:) - @project = project - @project = project - @from = from - @branch = branch - end - - def calculate_metric(name, start_time_attrs, end_time_attrs) - cte_table = Arel::Table.new("cte_table_for_#{name}") - - # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). - # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). - # We compute the (end_time - start_time) interval, and give it an alias based on the current - # cycle analytics stage. - interval_query = Arel::Nodes::As.new( - cte_table, - subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s)) - - median_datetime(cte_table, interval_query, name) - end - - # Join table with a row for every <issue,merge_request> pair (where the merge request - # closes the given issue) with issue and merge request metrics included. The metrics - # are loaded with an inner join, so issues / merge requests without metrics are - # automatically excluded. - def base_query_for(name) - # Load issues - query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). - join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). - where(issue_table[:project_id].eq(@project.id)). - where(issue_table[:deleted_at].eq(nil)). - where(issue_table[:created_at].gteq(@from)) - - query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch - - # Load merge_requests - query = query.join(mr_table, Arel::Nodes::OuterJoin). - on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). - join(mr_metrics_table). - on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) - - if DEPLOYMENT_METRIC_STAGES.include?(name) - # Limit to merge requests that have been deployed to production after `@from` - query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) - end - - query - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb similarity index 78% rename from lib/gitlab/cycle_analytics/plan_event.rb rename to lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 7c3f0e9989fbf3832d7f6217e1218179317ecf29..88a8710dbe670e4f303f7c509407098dc4dc57d8 100644 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -1,19 +1,17 @@ module Gitlab module CycleAnalytics - class PlanEvent < BaseEvent + class PlanEventFetcher < BaseEventFetcher def initialize(*args) - @stage = :plan - @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] - @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], - issue_metrics_table[:first_mentioned_in_commit_at]] @projections = [mr_diff_table[:st_commits].as('commits'), issue_metrics_table[:first_mentioned_in_commit_at]] super(*args) end - def custom_query(base_query) + def events_query base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..3b4dfc6a30e1a1e86f188477d1e69dd6435815b9 --- /dev/null +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class PlanStage < BaseStage + def start_time_attrs + @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end + + def end_time_attrs + @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end + + def name + :plan + end + + def description + "Time before an issue starts implementation" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb new file mode 100644 index 0000000000000000000000000000000000000000..0fa2e87f67358b7e366000e21696389a4ccaa575 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class ProductionEventFetcher < IssueEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..d693443bfa49012d5336541e09bb2aa8384d49c4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_helper.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module ProductionHelper + def stage_query + super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from])) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..2a6bcc80116ab24b12f89f7d76eb2ed4e9771026 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -0,0 +1,28 @@ +module Gitlab + module CycleAnalytics + class ProductionStage < BaseStage + include ProductionHelper + + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] + end + + def name + :production + end + + def description + "From issue creation until deploy to production" + end + + def query + # Limit to merge requests that have been deployed to production after `@from` + query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb similarity index 69% rename from lib/gitlab/cycle_analytics/code_event.rb rename to lib/gitlab/cycle_analytics/review_event_fetcher.rb index 2afdf0b8518b487d643c77fb77694373db601953..4df0bd06393e4140ffd71c8f5dddddb73b1d7bf6 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -1,25 +1,19 @@ module Gitlab module CycleAnalytics - class CodeEvent < BaseEvent + class ReviewEventFetcher < BaseEventFetcher include MergeRequestAllowed def initialize(*args) - @stage = :code - @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] - @end_time_attrs = mr_table[:created_at] @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], mr_table[:created_at], mr_table[:state], mr_table[:author_id]] - @order = mr_table[:created_at] super(*args) end - private - def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..fbaa3010d8138c96fc25179d367e0bfcca798bf1 --- /dev/null +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + class ReviewStage < BaseStage + def start_time_attrs + @start_time_attrs ||= mr_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:merged_at] + end + + def name + :review + end + + def description + "Time between merge request creation and merge/close" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..28e0455df59835d33c29480a0a65d2c8f2c6b918 --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module Stage + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb new file mode 100644 index 0000000000000000000000000000000000000000..b34baf5b081dd1299a3d8ccd28c2ab154a52897d --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -0,0 +1,23 @@ +module Gitlab + module CycleAnalytics + class StageSummary + def initialize(project, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + + def data + [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)), + serialize(Summary::Commit.new(project: @project, from: @from)), + serialize(Summary::Deploy.new(project: @project, from: @from))] + end + + private + + def serialize(summary_object) + AnalyticsSummarySerializer.new.represent(summary_object).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb similarity index 70% rename from lib/gitlab/cycle_analytics/staging_event.rb rename to lib/gitlab/cycle_analytics/staging_event_fetcher.rb index a1f30b716f6b1d170c6db873485010e678f4d185..a34731a5fcdb9ced2a6b42a584f3b1995fcff152 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -1,10 +1,7 @@ module Gitlab module CycleAnalytics - class StagingEvent < BaseEvent + class StagingEventFetcher < BaseEventFetcher def initialize(*args) - @stage = :staging - @start_time_attrs = mr_metrics_table[:merged_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [build_table[:id]] @order = build_table[:created_at] @@ -17,8 +14,10 @@ module Gitlab super end - def custom_query(base_query) + def events_query base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..945909a4d626b16ebef23c23fa5f45c1ec874038 --- /dev/null +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class StagingStage < BaseStage + include ProductionHelper + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:merged_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] + end + + def name + :staging + end + + def description + "From merge request merge until deploy to production" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..43fa3795e5ca98d7669202aaaee9c11829ad5399 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -0,0 +1,20 @@ +module Gitlab + module CycleAnalytics + module Summary + class Base + def initialize(project:, from:) + @project = project + @from = from + end + + def title + self.class.name.demodulize + end + + def value + raise NotImplementedError.new("Expected #{self.name} to implement value") + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..7b8faa4d854ed8e3d5b7324f60b9835f7620c7a7 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -0,0 +1,39 @@ +module Gitlab + module CycleAnalytics + module Summary + class Commit < Base + def value + @value ||= count_commits + end + + private + + # Don't use the `Gitlab::Git::Repository#log` method, because it enforces + # a limit. Since we need a commit count, we _can't_ enforce a limit, so + # the easiest way forward is to replicate the relevant portions of the + # `log` function here. + def count_commits + return unless ref + + repository = @project.repository.raw_repository + sha = @project.repository.commit(ref).sha + + cmd = %W(git --git-dir=#{repository.path} log) + cmd << '--format=%H' + cmd << "--after=#{@from.iso8601}" + cmd << sha + + output, status = Gitlab::Popen.popen(cmd) + + raise IOError, output unless status.zero? + + output.lines.count + end + + def ref + @ref ||= @project.default_branch.presence + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb new file mode 100644 index 0000000000000000000000000000000000000000..06032e9200edfdbcfb1f4c5574baacf0c849902b --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/deploy.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + module Summary + class Deploy < Base + def value + @value ||= @project.deployments.where("created_at > ?", @from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb new file mode 100644 index 0000000000000000000000000000000000000000..008468f24b90aedfd69b6a2c890ed2402bbf3c41 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/issue.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + module Summary + class Issue < Base + def initialize(project:, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + + def title + 'New Issue' + end + + def value + @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb deleted file mode 100644 index d553d0b5aecc8ac1dff64e3b7d86af45bf16e1cd..0000000000000000000000000000000000000000 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Gitlab - module CycleAnalytics - class TestEvent < StagingEvent - def initialize(*args) - super(*args) - - @stage = :test - @start_time_attrs = mr_metrics_table[:latest_build_started_at] - @end_time_attrs = mr_metrics_table[:latest_build_finished_at] - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2589c6601a2719d416d58fe68ea767dd91b8a28 --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class TestEventFetcher < StagingEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb new file mode 100644 index 0000000000000000000000000000000000000000..0079d56e0e4770ceb59262a78ea5ba6fb6d50627 --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -0,0 +1,29 @@ +module Gitlab + module CycleAnalytics + class TestStage < BaseStage + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:latest_build_started_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at] + end + + def name + :test + end + + def description + "Total test time for all commits/merges" + end + + def stage_query + if @options[:branch] + super.where(build_table[:ref].eq(@options[:branch])) + else + super + end + end + end + end +end diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 1444d25ebc7563540ded9cd95846158ad369a349..08607c27c0987470f9131a188faa44257811de6d 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -103,6 +103,11 @@ module Gitlab Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) end + def extract_diff_epoch(diff) + return diff unless Gitlab::Database.postgresql? + + Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))}) + end # Need to cast '0' to an INTERVAL before we can check if the interval is positive def zero_interval Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index 56530448f3603475b68161bf684d40a8df20f9ea..329d12f13d161210356d1d6342c01b2ca77b85c1 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -61,7 +61,10 @@ module Gitlab end def cacheable?(diff_file) - @merge_request_diff.present? && diff_file.blob && diff_file.blob.text? + @merge_request_diff.present? && + diff_file.blob && + diff_file.blob.text? && + @project.repository.diffable?(diff_file.blob) end def cache_key diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 85402c2a278b6ffa9f153618f9e9763a22e28e33..8c8dd1b9cef492395cd0d216d2b8d3a9c3200885 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -13,9 +13,17 @@ module Gitlab encoding = body.encoding - body = discourse_email_trimmer(body) + body = EmailReplyTrimmer.trim(body) - body = EmailReplyParser.parse_reply(body) + return '' unless body + + # not using /\s+$/ here because that deletes empty lines + body = body.gsub(/[ \t]$/, '') + + # NOTE: We currently don't support empty quotes. + # EmailReplyTrimmer allows this as a special case, + # so we detect it manually here. + return "" if body.lines.all? { |l| l.strip.empty? || l.start_with?('>') } body.force_encoding(encoding).encode("UTF-8") end @@ -57,30 +65,6 @@ module Gitlab rescue nil end - - REPLYING_HEADER_LABELS = %w(From Sent To Subject Reply To Cc Bcc Date) - REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |label| "#{label}:" }) - - def discourse_email_trimmer(body) - lines = body.scrub.lines.to_a - range_end = 0 - - lines.each_with_index do |l, idx| - # This one might be controversial but so many reply lines have years, times and end with a colon. - # Let's try it and see how well it works. - break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) || - (l =~ /On \w+ \d+,? \d+,?.*wrote:/) - - # Headers on subsequent lines - break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX } - # Headers on the same line - break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3 - - range_end = idx - end - - lines[0..range_end].join.strip - end end end end diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index a7c596dced0198f00de097ffe3b3ea5f54cc6860..b984492d369b6e3ae3bb50cbee2e4fbbf00aa0a0 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -76,7 +76,7 @@ module Gitlab if referable.respond_to?(:project) referable.to_reference(target_project) else - referable.to_reference(@source_project, target_project) + referable.to_reference(@source_project, target_project: target_project) end end diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb index abc8c8c55e6f130952078f59231491318ef6ee4d..8fab54896160c69119f5cd646239df43a977916d 100644 --- a/lib/gitlab/gfm/uploads_rewriter.rb +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -1,3 +1,5 @@ +require 'fileutils' + module Gitlab module Gfm ## @@ -22,7 +24,9 @@ module Gitlab return markdown unless file.try(:exists?) new_uploader = FileUploader.new(target_project) - new_uploader.store!(file) + with_link_in_tmp_dir(file.file) do |open_tmp_file| + new_uploader.store!(open_tmp_file) + end new_uploader.to_markdown end end @@ -46,6 +50,19 @@ module Gitlab uploader.retrieve_from_store!(file) uploader.file end + + # Because the uploaders use 'move_to_store' we must have a temporary + # file that is allowed to be (re)moved. + def with_link_in_tmp_dir(file) + dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file)) + # The filename matters to Carrierwave so we make sure to preserve it + tmp_file = File.join(dir, File.basename(file)) + File.link(file, tmp_file) + # Open the file to placate Carrierwave + File.open(tmp_file) { |open_file| yield open_file } + ensure + FileUtils.rm_rf(dir) + end end end end diff --git a/lib/gitlab/git/attributes.rb b/lib/gitlab/git/attributes.rb new file mode 100644 index 0000000000000000000000000000000000000000..42140ecc9932c3649c38080abfa81a6fab056fdc --- /dev/null +++ b/lib/gitlab/git/attributes.rb @@ -0,0 +1,131 @@ +module Gitlab + module Git + # Class for parsing Git attribute files and extracting the attributes for + # file patterns. + # + # Unlike Rugged this parser only needs a single IO call (a call to `open`), + # vastly reducing the time spent in extracting attributes. + # + # This class _only_ supports parsing the attributes file located at + # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files + # (`.gitattributes` is copied to this particular path). + # + # Basic usage: + # + # attributes = Gitlab::Git::Attributes.new(some_repo.path) + # + # attributes.attributes('README.md') # => { "eol" => "lf } + class Attributes + # path - The path to the Git repository. + def initialize(path) + @path = File.expand_path(path) + @patterns = nil + end + + # Returns all the Git attributes for the given path. + # + # path - A path to a file for which to get the attributes. + # + # Returns a Hash. + def attributes(path) + full_path = File.join(@path, path) + + patterns.each do |pattern, attrs| + return attrs if File.fnmatch?(pattern, full_path) + end + + {} + end + + # Returns a Hash containing the file patterns and their attributes. + def patterns + @patterns ||= parse_file + end + + # Parses an attribute string. + # + # These strings can be in the following formats: + # + # text # => { "text" => true } + # -text # => { "text" => false } + # key=value # => { "key" => "value" } + # + # string - The string to parse. + # + # Returns a Hash containing the attributes and their values. + def parse_attributes(string) + values = {} + dash = '-' + equal = '=' + binary = 'binary' + + string.split(/\s+/).each do |chunk| + # Data such as "foo = bar" should be treated as "foo" and "bar" being + # separate boolean attributes. + next if chunk == equal + + key = chunk + + # Input: "-foo" + if chunk.start_with?(dash) + key = chunk.byteslice(1, chunk.length - 1) + value = false + + # Input: "foo=bar" + elsif chunk.include?(equal) + key, value = chunk.split(equal, 2) + + # Input: "foo" + else + value = true + end + + values[key] = value + + # When the "binary" option is set the "diff" option should be set to + # the inverse. If "diff" is later set it should overwrite the + # automatically set value. + values['diff'] = false if key == binary && value + end + + values + end + + # Iterates over every line in the attributes file. + def each_line + full_path = File.join(@path, 'info/attributes') + + return unless File.exist?(full_path) + + File.open(full_path, 'r') do |handle| + handle.each_line do |line| + break unless line.valid_encoding? + + yield line.strip + end + end + end + + private + + # Parses the Git attributes file. + def parse_file + pairs = [] + comment = '#' + + each_line do |line| + next if line.start_with?(comment) || line.empty? + + pattern, attrs = line.split(/\s+/, 2) + + parsed = attrs ? parse_attributes(attrs) : {} + + pairs << [File.join(@path, pattern), parsed] + end + + # Newer entries take precedence over older entries. + pairs.reverse.to_h + end + end + end +end diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb new file mode 100644 index 0000000000000000000000000000000000000000..581933919265a7743260b2dadc026dcbb4108b66 --- /dev/null +++ b/lib/gitlab/git/blame.rb @@ -0,0 +1,75 @@ +module Gitlab + module Git + class Blame + include Gitlab::Git::EncodingHelper + + attr_reader :lines, :blames + + def initialize(repository, sha, path) + @repo = repository + @sha = sha + @path = path + @lines = [] + @blames = load_blame + end + + def each + @blames.each do |blame| + yield( + Gitlab::Git::Commit.new(blame.commit), + blame.line + ) + end + end + + private + + def load_blame + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) + # Read in binary mode to ensure ASCII-8BIT + raw_output = IO.popen(cmd, 'rb') {|io| io.read } + output = encode_utf8(raw_output) + process_raw_blame output + end + + def process_raw_blame(output) + lines, final = [], [] + info, commits = {}, {} + + # process the output + output.split("\n").each do |line| + if line[0, 1] == "\t" + lines << line[1, line.size] + elsif m = /^(\w{40}) (\d+) (\d+)/.match(line) + commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i + commits[commit_id] = nil unless commits.key?(commit_id) + info[lineno] = [commit_id, old_lineno] + end + end + + # load all commits in single call + commits.keys.each do |key| + commits[key] = @repo.lookup(key) + end + + # get it together + info.sort.each do |lineno, (commit_id, old_lineno)| + commit = commits[commit_id] + final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1]) + end + + @lines = final + end + end + + class BlameLine + attr_accessor :lineno, :oldlineno, :commit, :line + def initialize(lineno, oldlineno, commit, line) + @lineno = lineno + @oldlineno = oldlineno + @commit = commit + @line = line + end + end + end +end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb new file mode 100644 index 0000000000000000000000000000000000000000..b742d9e1e4bd2ba35ac9b7887301702b9931f2e0 --- /dev/null +++ b/lib/gitlab/git/blob.rb @@ -0,0 +1,330 @@ +module Gitlab + module Git + class Blob + include Linguist::BlobHelper + include Gitlab::Git::EncodingHelper + + # This number is the maximum amount of data that we want to display to + # the user. We load as much as we can for encoding detection + # (Linguist) and LFS pointer parsing. All other cases where we need full + # blob data should use load_all_data!. + MAX_DATA_DISPLAY_SIZE = 10485760 + + attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary + + class << self + def find(repository, sha, path) + commit = repository.lookup(sha) + root_tree = commit.tree + + blob_entry = find_entry_by_path(repository, root_tree.oid, path) + + return nil unless blob_entry + + if blob_entry[:type] == :commit + submodule_blob(blob_entry, path, sha) + else + blob = repository.lookup(blob_entry[:oid]) + + if blob + new( + id: blob.oid, + name: blob_entry[:name], + size: blob.size, + data: blob.content(MAX_DATA_DISPLAY_SIZE), + mode: blob_entry[:filemode].to_s(8), + path: path, + commit_id: sha, + binary: blob.binary? + ) + end + end + end + + def raw(repository, sha) + blob = repository.lookup(sha) + + new( + id: blob.oid, + size: blob.size, + data: blob.content(MAX_DATA_DISPLAY_SIZE), + binary: blob.binary? + ) + end + + # Recursive search of blob id by path + # + # Ex. + # blog/ # oid: 1a + # app/ # oid: 2a + # models/ # oid: 3a + # file.rb # oid: 4a + # + # + # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a' + # + def find_entry_by_path(repository, root_id, path) + root_tree = repository.lookup(root_id) + # Strip leading slashes + path[/^\/*/] = '' + path_arr = path.split('/') + + entry = root_tree.find do |entry| + entry[:name] == path_arr[0] + end + + return nil unless entry + + if path_arr.size > 1 + return nil unless entry[:type] == :tree + path_arr.shift + find_entry_by_path(repository, entry[:oid], path_arr.join('/')) + else + [:blob, :commit].include?(entry[:type]) ? entry : nil + end + end + + def submodule_blob(blob_entry, path, sha) + new( + id: blob_entry[:oid], + name: blob_entry[:name], + data: '', + path: path, + commit_id: sha, + ) + end + + # Commit file in repository and return commit sha + # + # options should contain next structure: + # file: { + # content: 'Lorem ipsum...', + # path: 'documents/story.txt', + # update: true + # }, + # author: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # committer: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # commit: { + # message: 'Wow such commit', + # branch: 'master', + # update_ref: false + # } + # + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def commit(repository, options, action = :add) + file = options[:file] + update = file[:update].nil? ? true : file[:update] + author = options[:author] + committer = options[:committer] + commit = options[:commit] + repo = repository.rugged + ref = commit[:branch] + update_ref = commit[:update_ref].nil? ? true : commit[:update_ref] + parents = [] + mode = 0o100644 + + unless ref.start_with?('refs/') + ref = 'refs/heads/' + ref + end + + path_name = Gitlab::Git::PathHelper.normalize_path(file[:path]) + # Abort if any invalid characters remain (e.g. ../foo) + raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..') + + filename = path_name.to_s + index = repo.index + + unless repo.empty? + rugged_ref = repo.references[ref] + raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref + last_commit = rugged_ref.target + index.read_tree(last_commit.tree) + parents = [last_commit] + end + + if action == :remove + index.remove(filename) + else + file_entry = index.get(filename) + + if action == :rename + old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path]) + old_filename = old_path_name.to_s + file_entry = index.get(old_filename) + index.remove(old_filename) unless file_entry.blank? + end + + if file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update + + # Preserve the current file mode if one is available + mode = file_entry[:mode] if file_entry[:mode] + end + + content = file[:content] + detect = CharlockHolmes::EncodingDetector.new.detect(content) if content + + unless detect && detect[:type] == :binary + # When writing to the repo directly as we are doing here, + # the `core.autocrlf` config isn't taken into account. + content.gsub!("\r\n", "\n") if repository.autocrlf + end + + oid = repo.write(content, :blob) + index.add(path: filename, oid: oid, mode: mode) + end + + opts = {} + opts[:tree] = index.write_tree(repo) + opts[:author] = author + opts[:committer] = committer + opts[:message] = commit[:message] + opts[:parents] = parents + opts[:update_ref] = ref if update_ref + + Rugged::Commit.create(repo, opts) + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/PerceivedComplexity + + # Remove file from repository and return commit sha + # + # options should contain next structure: + # file: { + # path: 'documents/story.txt' + # }, + # author: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # committer: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # commit: { + # message: 'Remove FILENAME', + # branch: 'master' + # } + # + def remove(repository, options) + commit(repository, options, :remove) + end + + # Rename file from repository and return commit sha + # + # options should contain next structure: + # file: { + # previous_path: 'documents/old_story.txt' + # path: 'documents/story.txt' + # content: 'Lorem ipsum...', + # update: true + # }, + # author: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # committer: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # commit: { + # message: 'Rename FILENAME', + # branch: 'master' + # } + # + def rename(repository, options) + commit(repository, options, :rename) + end + end + + def initialize(options) + %w(id name path size data mode commit_id binary).each do |key| + self.send("#{key}=", options[key.to_sym]) + end + + @loaded_all_data = false + # Retain the actual size before it is encoded + @loaded_size = @data.bytesize if @data + end + + def binary? + @binary.nil? ? super : @binary == true + end + + def empty? + !data || data == '' + end + + def data + encode! @data + end + + # Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into + # memory as a Ruby string. + def load_all_data!(repository) + return if @data == '' # don't mess with submodule blobs + return @data if @loaded_all_data + + @loaded_all_data = true + @data = repository.lookup(id).content + @loaded_size = @data.bytesize + end + + def name + encode! @name + end + + # Valid LFS object pointer is a text file consisting of + # version + # oid + # size + # see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer + def lfs_pointer? + has_lfs_version_key? && lfs_oid.present? && lfs_size.present? + end + + def lfs_oid + if has_lfs_version_key? + oid = data.match(/(?<=sha256:)([0-9a-f]{64})/) + return oid[1] if oid + end + + nil + end + + def lfs_size + if has_lfs_version_key? + size = data.match(/(?<=size )([0-9]+)/) + return size[1] if size + end + + nil + end + + def truncated? + size && (size > loaded_size) + end + + private + + def has_lfs_version_key? + !empty? && text? && data.start_with?("version https://git-lfs.github.com/spec") + end + end + end +end diff --git a/lib/gitlab/git/blob_snippet.rb b/lib/gitlab/git/blob_snippet.rb new file mode 100644 index 0000000000000000000000000000000000000000..e98de57fc22b893d8203448fb927b50e3128d74f --- /dev/null +++ b/lib/gitlab/git/blob_snippet.rb @@ -0,0 +1,32 @@ +module Gitlab + module Git + class BlobSnippet + include Linguist::BlobHelper + + attr_accessor :ref + attr_accessor :lines + attr_accessor :filename + attr_accessor :startline + + def initialize(ref, lines, startline, filename) + @ref, @lines, @startline, @filename = ref, lines, startline, filename + end + + def data + lines.join("\n") if lines + end + + def name + filename + end + + def size + data.length + end + + def mode + nil + end + end + end +end diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb new file mode 100644 index 0000000000000000000000000000000000000000..586380da94a3f5e1a338093a3469a51ebc6e93f3 --- /dev/null +++ b/lib/gitlab/git/branch.rb @@ -0,0 +1,6 @@ +module Gitlab + module Git + class Branch < Ref + end + end +end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..d785516ebdda1dad1dfdf71c99271b4c6047ffa0 --- /dev/null +++ b/lib/gitlab/git/commit.rb @@ -0,0 +1,310 @@ +# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object +module Gitlab + module Git + class Commit + include Gitlab::Git::EncodingHelper + + attr_accessor :raw_commit, :head, :refs + + SERIALIZE_KEYS = [ + :id, :message, :parent_ids, + :authored_date, :author_name, :author_email, + :committed_date, :committer_name, :committer_email + ].freeze + + attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator + + def ==(other) + return false unless other.is_a?(Gitlab::Git::Commit) + + methods = [:message, :parent_ids, :authored_date, :author_name, + :author_email, :committed_date, :committer_name, + :committer_email] + + methods.all? do |method| + send(method) == other.send(method) + end + end + + class << self + # Get commits collection + # + # Ex. + # Commit.where( + # repo: repo, + # ref: 'master', + # path: 'app/models', + # limit: 10, + # offset: 5, + # ) + # + def where(options) + repo = options.delete(:repo) + raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log) + + repo.log(options).map { |c| decorate(c) } + end + + # Get single commit + # + # Ex. + # Commit.find(repo, '29eda46b') + # + # Commit.find(repo, 'master') + # + def find(repo, commit_id = "HEAD") + return decorate(commit_id) if commit_id.is_a?(Rugged::Commit) + + obj = if commit_id.is_a?(String) + repo.rev_parse_target(commit_id) + else + Gitlab::Git::Ref.dereference_object(commit_id) + end + + return nil unless obj.is_a?(Rugged::Commit) + + decorate(obj) + rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository + nil + end + + # Get last commit for HEAD + # + # Ex. + # Commit.last(repo) + # + def last(repo) + find(repo) + end + + # Get last commit for specified path and ref + # + # Ex. + # Commit.last_for_path(repo, '29eda46b', 'app/models') + # + # Commit.last_for_path(repo, 'master', 'Gemfile') + # + def last_for_path(repo, ref, path = nil) + where( + repo: repo, + ref: ref, + path: path, + limit: 1 + ).first + end + + # Get commits between two revspecs + # See also #repository.commits_between + # + # Ex. + # Commit.between(repo, '29eda46b', 'master') + # + def between(repo, base, head) + repo.commits_between(base, head).map do |commit| + decorate(commit) + end + rescue Rugged::ReferenceError + [] + end + + # Delegate Repository#find_commits + def find_all(repo, options = {}) + repo.find_commits(options) + end + + def decorate(commit, ref = nil) + Gitlab::Git::Commit.new(commit, ref) + end + + # Returns a diff object for the changes introduced by +rugged_commit+. + # If +rugged_commit+ doesn't have a parent, then the diff is between + # this commit and an empty repo. See Repository#diff for the keys + # allowed in the +options+ hash. + def diff_from_parent(rugged_commit, options = {}) + options ||= {} + break_rewrites = options[:break_rewrites] + actual_options = Gitlab::Git::Diff.filter_diff_options(options) + + diff = if rugged_commit.parents.empty? + rugged_commit.diff(actual_options.merge(reverse: true)) + else + rugged_commit.parents[0].diff(rugged_commit, actual_options) + end + + diff.find_similar!(break_rewrites: break_rewrites) + diff + end + end + + def initialize(raw_commit, head = nil) + raise "Nil as raw commit passed" unless raw_commit + + if raw_commit.is_a?(Hash) + init_from_hash(raw_commit) + elsif raw_commit.is_a?(Rugged::Commit) + init_from_rugged(raw_commit) + else + raise "Invalid raw commit type: #{raw_commit.class}" + end + + @head = head + end + + def sha + id + end + + def short_id(length = 10) + id.to_s[0..length] + end + + def safe_message + @safe_message ||= message + end + + def created_at + committed_date + end + + # Was this commit committed by a different person than the original author? + def different_committer? + author_name != committer_name || author_email != committer_email + end + + def parent_id + parent_ids.first + end + + # Shows the diff between the commit's parent and the commit. + # + # Cuts out the header and stats from #to_patch and returns only the diff. + def to_diff(options = {}) + diff_from_parent(options).patch + end + + # Returns a diff object for the changes from this commit's first parent. + # If there is no parent, then the diff is between this commit and an + # empty repo. See Repository#diff for keys allowed in the +options+ + # hash. + def diff_from_parent(options = {}) + Commit.diff_from_parent(raw_commit, options) + end + + def has_zero_stats? + stats.total.zero? + rescue + true + end + + def no_commit_message + "--no commit message" + end + + def to_hash + serialize_keys.map.with_object({}) do |key, hash| + hash[key] = send(key) + end + end + + def date + committed_date + end + + def diffs(options = {}) + Gitlab::Git::DiffCollection.new(diff_from_parent(options), options) + end + + def parents + raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) } + end + + def tree + raw_commit.tree + end + + def stats + Gitlab::Git::CommitStats.new(self) + end + + def to_patch(options = {}) + begin + raw_commit.to_mbox(options) + rescue Rugged::InvalidError => ex + if ex.message =~ /Commit \w+ is a merge commit/ + 'Patch format is not currently supported for merge commits.' + end + end + end + + # Get a collection of Rugged::Reference objects for this commit. + # + # Ex. + # commit.ref(repo) + # + def refs(repo) + repo.refs_hash[id] + end + + # Get ref names collection + # + # Ex. + # commit.ref_names(repo) + # + def ref_names(repo) + refs(repo).map do |ref| + ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "") + end + end + + def message + encode! @message + end + + def author_name + encode! @author_name + end + + def author_email + encode! @author_email + end + + def committer_name + encode! @committer_name + end + + def committer_email + encode! @committer_email + end + + private + + def init_from_hash(hash) + raw_commit = hash.symbolize_keys + + serialize_keys.each do |key| + send("#{key}=", raw_commit[key]) + end + end + + def init_from_rugged(commit) + author = commit.author + committer = commit.committer + + @raw_commit = commit + @id = commit.oid + @message = commit.message + @authored_date = author[:time] + @committed_date = committer[:time] + @author_name = author[:name] + @author_email = author[:email] + @committer_name = committer[:name] + @committer_email = committer[:email] + @parent_ids = commit.parents.map(&:oid) + end + + def serialize_keys + SERIALIZE_KEYS + end + end + end +end diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb new file mode 100644 index 0000000000000000000000000000000000000000..e9118bbed0e07e6a3f863956151a810cf238f54d --- /dev/null +++ b/lib/gitlab/git/commit_stats.rb @@ -0,0 +1,26 @@ +# Gitlab::Git::CommitStats counts the additions, deletions, and total changes +# in a commit. +module Gitlab + module Git + class CommitStats + attr_reader :id, :additions, :deletions, :total + + # Instantiate a CommitStats object + def initialize(commit) + @id = commit.id + @additions = 0 + @deletions = 0 + @total = 0 + + diff = commit.diff_from_parent + + diff.each_patch do |p| + # TODO: Use the new Rugged convenience methods when they're released + @additions += p.stat[0] + @deletions += p.stat[1] + @total += p.changes + end + end + end + end +end diff --git a/lib/gitlab/git/compare.rb b/lib/gitlab/git/compare.rb new file mode 100644 index 0000000000000000000000000000000000000000..696a2acd5e345a5af79b162b906f3ad8eab16992 --- /dev/null +++ b/lib/gitlab/git/compare.rb @@ -0,0 +1,43 @@ +module Gitlab + module Git + class Compare + attr_reader :head, :base, :straight + + def initialize(repository, base, head, straight = false) + @repository = repository + @straight = straight + + unless base && head + @commits = [] + return + end + + @base = Gitlab::Git::Commit.find(repository, base.try(:strip)) + @head = Gitlab::Git::Commit.find(repository, head.try(:strip)) + + @commits = [] unless @base && @head + @commits = [] if same + end + + def same + @base && @head && @base.id == @head.id + end + + def commits + return @commits if defined?(@commits) + + @commits = Gitlab::Git::Commit.between(@repository, @base.id, @head.id) + end + + def diffs(options = {}) + unless @head && @base + return Gitlab::Git::DiffCollection.new([]) + end + + paths = options.delete(:paths) || [] + options[:straight] = @straight + Gitlab::Git::Diff.between(@repository, @head.id, @base.id, options, *paths) + end + end + end +end diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb new file mode 100644 index 0000000000000000000000000000000000000000..d6b3b5705a9a764c23bc24b8a1a551ad1675769f --- /dev/null +++ b/lib/gitlab/git/diff.rb @@ -0,0 +1,322 @@ +# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object +module Gitlab + module Git + class Diff + class TimeoutError < StandardError; end + include Gitlab::Git::EncodingHelper + + # Diff properties + attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff + + # Stats properties + attr_accessor :new_file, :renamed_file, :deleted_file + + attr_accessor :too_large + + # The maximum size of a diff to display. + DIFF_SIZE_LIMIT = 102400 # 100 KB + + # The maximum size before a diff is collapsed. + DIFF_COLLAPSE_LIMIT = 10240 # 10 KB + + class << self + def between(repo, head, base, options = {}, *paths) + straight = options.delete(:straight) || false + + common_commit = if straight + base + else + # Only show what is new in the source branch + # compared to the target branch, not the other way + # around. The linex below with merge_base is + # equivalent to diff with three dots (git diff + # branch1...branch2) From the git documentation: + # "git diff A...B" is equivalent to "git diff + # $(git-merge-base A B) B" + repo.merge_base_commit(head, base) + end + + options ||= {} + actual_options = filter_diff_options(options) + repo.diff(common_commit, head, actual_options, *paths) + end + + # Return a copy of the +options+ hash containing only keys that can be + # passed to Rugged. Allowed options are: + # + # :max_size :: + # An integer specifying the maximum byte size of a file before a it + # will be treated as binary. The default value is 512MB. + # + # :context_lines :: + # The number of unchanged lines that define the boundary of a hunk + # (and to display before and after the actual changes). The default is + # 3. + # + # :interhunk_lines :: + # The maximum number of unchanged lines between hunk boundaries before + # the hunks will be merged into a one. The default is 0. + # + # :old_prefix :: + # The virtual "directory" to prefix to old filenames in hunk headers. + # The default is "a". + # + # :new_prefix :: + # The virtual "directory" to prefix to new filenames in hunk headers. + # The default is "b". + # + # :reverse :: + # If true, the sides of the diff will be reversed. + # + # :force_text :: + # If true, all files will be treated as text, disabling binary + # attributes & detection. + # + # :ignore_whitespace :: + # If true, all whitespace will be ignored. + # + # :ignore_whitespace_change :: + # If true, changes in amount of whitespace will be ignored. + # + # :ignore_whitespace_eol :: + # If true, whitespace at end of line will be ignored. + # + # :ignore_submodules :: + # if true, submodules will be excluded from the diff completely. + # + # :patience :: + # If true, the "patience diff" algorithm will be used (currenlty + # unimplemented). + # + # :include_ignored :: + # If true, ignored files will be included in the diff. + # + # :include_untracked :: + # If true, untracked files will be included in the diff. + # + # :include_unmodified :: + # If true, unmodified files will be included in the diff. + # + # :recurse_untracked_dirs :: + # Even if +:include_untracked+ is true, untracked directories will + # only be marked with a single entry in the diff. If this flag is set + # to true, all files under ignored directories will be included in the + # diff, too. + # + # :disable_pathspec_match :: + # If true, the given +*paths+ will be applied as exact matches, + # instead of as fnmatch patterns. + # + # :deltas_are_icase :: + # If true, filename comparisons will be made with case-insensitivity. + # + # :include_untracked_content :: + # if true, untracked content will be contained in the the diff patch + # text. + # + # :skip_binary_check :: + # If true, diff deltas will be generated without spending time on + # binary detection. This is useful to improve performance in cases + # where the actual file content difference is not needed. + # + # :include_typechange :: + # If true, type changes for files will not be interpreted as deletion + # of the "old file" and addition of the "new file", but will generate + # typechange records. + # + # :include_typechange_trees :: + # Even if +:include_typechange+ is true, blob -> tree changes will + # still usually be handled as a deletion of the blob. If this flag is + # set to true, blob -> tree changes will be marked as typechanges. + # + # :ignore_filemode :: + # If true, file mode changes will be ignored. + # + # :recurse_ignored_dirs :: + # Even if +:include_ignored+ is true, ignored directories will only be + # marked with a single entry in the diff. If this flag is set to true, + # all files under ignored directories will be included in the diff, + # too. + def filter_diff_options(options, default_options = {}) + allowed_options = [:max_size, :context_lines, :interhunk_lines, + :old_prefix, :new_prefix, :reverse, :force_text, + :ignore_whitespace, :ignore_whitespace_change, + :ignore_whitespace_eol, :ignore_submodules, + :patience, :include_ignored, :include_untracked, + :include_unmodified, :recurse_untracked_dirs, + :disable_pathspec_match, :deltas_are_icase, + :include_untracked_content, :skip_binary_check, + :include_typechange, :include_typechange_trees, + :ignore_filemode, :recurse_ignored_dirs, :paths, + :max_files, :max_lines, :all_diffs, :no_collapse] + + if default_options + actual_defaults = default_options.dup + actual_defaults.keep_if do |key| + allowed_options.include?(key) + end + else + actual_defaults = {} + end + + if options + filtered_opts = options.dup + filtered_opts.keep_if do |key| + allowed_options.include?(key) + end + filtered_opts = actual_defaults.merge(filtered_opts) + else + filtered_opts = actual_defaults + end + + filtered_opts + end + end + + def initialize(raw_diff, collapse: false) + case raw_diff + when Hash + init_from_hash(raw_diff, collapse: collapse) + when Rugged::Patch, Rugged::Diff::Delta + init_from_rugged(raw_diff, collapse: collapse) + when nil + raise "Nil as raw diff passed" + else + raise "Invalid raw diff type: #{raw_diff.class}" + end + end + + def serialize_keys + @serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large) + end + + def to_hash + hash = {} + + keys = serialize_keys + + keys.each do |key| + hash[key] = send(key) + end + + hash + end + + def submodule? + a_mode == '160000' || b_mode == '160000' + end + + def line_count + @line_count ||= Util.count_lines(@diff) + end + + def too_large? + if @too_large.nil? + @too_large = @diff.bytesize >= DIFF_SIZE_LIMIT + else + @too_large + end + end + + def collapsible? + @diff.bytesize >= DIFF_COLLAPSE_LIMIT + end + + def prune_large_diff! + @diff = '' + @line_count = 0 + @too_large = true + end + + def collapsed? + return @collapsed if defined?(@collapsed) + false + end + + def prune_collapsed_diff! + @diff = '' + @line_count = 0 + @collapsed = true + end + + private + + def init_from_rugged(rugged, collapse: false) + if rugged.is_a?(Rugged::Patch) + init_from_rugged_patch(rugged, collapse: collapse) + d = rugged.delta + else + d = rugged + end + + @new_path = encode!(d.new_file[:path]) + @old_path = encode!(d.old_file[:path]) + @a_mode = d.old_file[:mode].to_s(8) + @b_mode = d.new_file[:mode].to_s(8) + @new_file = d.added? + @renamed_file = d.renamed? + @deleted_file = d.deleted? + end + + def init_from_rugged_patch(patch, collapse: false) + # Don't bother initializing diffs that are too large. If a diff is + # binary we're not going to display anything so we skip the size check. + return if !patch.delta.binary? && prune_large_patch(patch, collapse) + + @diff = encode!(strip_diff_headers(patch.to_s)) + end + + def init_from_hash(hash, collapse: false) + raw_diff = hash.symbolize_keys + + serialize_keys.each do |key| + send(:"#{key}=", raw_diff[key.to_sym]) + end + + prune_large_diff! if too_large? + prune_collapsed_diff! if collapse && collapsible? + end + + # If the patch surpasses any of the diff limits it calls the appropiate + # prune method and returns true. Otherwise returns false. + def prune_large_patch(patch, collapse) + size = 0 + + patch.each_hunk do |hunk| + hunk.each_line do |line| + size += line.content.bytesize + + if size >= DIFF_SIZE_LIMIT + prune_large_diff! + return true + end + end + end + + if collapse && size >= DIFF_COLLAPSE_LIMIT + prune_collapsed_diff! + return true + end + + false + end + + # Strip out the information at the beginning of the patch's text to match + # Grit's output + def strip_diff_headers(diff_text) + # Delete everything up to the first line that starts with '---' or + # 'Binary' + diff_text.sub!(/\A.*?^(---|Binary)/m, '\1') + + if diff_text.start_with?('---', 'Binary') + diff_text + else + # If the diff_text did not contain a line starting with '---' or + # 'Binary', return the empty string. No idea why; we are just + # preserving behavior from before the refactor. + '' + end + end + end + end +end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb new file mode 100644 index 0000000000000000000000000000000000000000..65e06f5065dbe83ab9b14be856b0d4b64183c071 --- /dev/null +++ b/lib/gitlab/git/diff_collection.rb @@ -0,0 +1,129 @@ +module Gitlab + module Git + class DiffCollection + include Enumerable + + DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze + + def initialize(iterator, options = {}) + @iterator = iterator + @max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files]) + @max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines]) + @max_bytes = @max_files * 5120 # Average 5 KB per file + @safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min + @safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min + @safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file + @all_diffs = !!options.fetch(:all_diffs, false) + @no_collapse = !!options.fetch(:no_collapse, true) + @deltas_only = !!options.fetch(:deltas_only, false) + + @line_count = 0 + @byte_count = 0 + @overflow = false + @array = Array.new + end + + def each(&block) + if @populated + # @iterator.each is slower than just iterating the array in place + @array.each(&block) + elsif @deltas_only + each_delta(&block) + else + each_patch(&block) + end + end + + def empty? + !@iterator.any? + end + + def overflow? + populate! + !!@overflow + end + + def size + @size ||= count # forces a loop using each method + end + + def real_size + populate! + + if @overflow + "#{size}+" + else + size.to_s + end + end + + def decorate! + collection = each_with_index do |element, i| + @array[i] = yield(element) + end + @populated = true + collection + end + + private + + def populate! + return if @populated + + each { nil } # force a loop through all diffs + @populated = true + nil + end + + def over_safe_limits?(files) + files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes + end + + def each_delta + @iterator.each_delta.with_index do |delta, i| + diff = Gitlab::Git::Diff.new(delta) + + yield @array[i] = diff + end + end + + def each_patch + @iterator.each_with_index do |raw, i| + # First yield cached Diff instances from @array + if @array[i] + yield @array[i] + next + end + + # We have exhausted @array, time to create new Diff instances or stop. + break if @overflow + + if !@all_diffs && i >= @max_files + @overflow = true + break + end + + collapse = !@all_diffs && !@no_collapse + + diff = Gitlab::Git::Diff.new(raw, collapse: collapse) + + if collapse && over_safe_limits?(i) + diff.prune_collapsed_diff! + end + + @line_count += diff.line_count + @byte_count += diff.diff.bytesize + + if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes) + # This last Diff instance pushes us over the lines limit. We stop and + # discard it. + @overflow = true + break + end + + yield @array[i] = diff + end + end + end + end +end diff --git a/lib/gitlab/git/encoding_helper.rb b/lib/gitlab/git/encoding_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..e57d228e688a652a0334d09398d647e352f9be4b --- /dev/null +++ b/lib/gitlab/git/encoding_helper.rb @@ -0,0 +1,58 @@ +module Gitlab + module Git + module EncodingHelper + extend self + + # This threshold is carefully tweaked to prevent usage of encodings detected + # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low, + # we're better off sticking with utf8 encoding. + # Reason: git diff can return strings with invalid utf8 byte sequences if it + # truncates a diff in the middle of a multibyte character. In this case + # CharlockHolmes will try to guess the encoding and will likely suggest an + # obscure encoding with low confidence. + # There is a lot more info with this merge request: + # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193 + ENCODING_CONFIDENCE_THRESHOLD = 40 + + def encode!(message) + return nil unless message.respond_to? :force_encoding + + # if message is utf-8 encoding, just return it + message.force_encoding("UTF-8") + return message if message.valid_encoding? + + # return message if message type is binary + detect = CharlockHolmes::EncodingDetector.detect(message) + return message.force_encoding("BINARY") if detect && detect[:type] == :binary + + # force detected encoding if we have sufficient confidence. + if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD + message.force_encoding(detect[:encoding]) + end + + # encode and clean the bad chars + message.replace clean(message) + rescue + encoding = detect ? detect[:encoding] : "unknown" + "--broken encoding: #{encoding}" + end + + def encode_utf8(message) + detect = CharlockHolmes::EncodingDetector.detect(message) + if detect + CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8') + else + clean(message) + end + end + + private + + def clean(message) + message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "") + .encode("UTF-8") + .gsub("\0".encode("UTF-8"), "") + end + end + end +end diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..0148cd8df056a2169579dcc870c9a808301c0292 --- /dev/null +++ b/lib/gitlab/git/path_helper.rb @@ -0,0 +1,16 @@ +module Gitlab + module Git + class PathHelper + class << self + def normalize_path(filename) + # Strip all leading slashes so that //foo -> foo + filename[/^\/*/] = '' + + # Expand relative paths (e.g. foo/../bar) + filename = Pathname.new(filename) + filename.relative_path_from(Pathname.new('')) + end + end + end + end +end diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb new file mode 100644 index 0000000000000000000000000000000000000000..df9ca3ee5ac94ca64024d1ffd53fd8bfb5865c02 --- /dev/null +++ b/lib/gitlab/git/popen.rb @@ -0,0 +1,26 @@ +require 'open3' + +module Gitlab + module Git + module Popen + def popen(cmd, path) + unless cmd.is_a?(Array) + raise "System commands must be given as an array of strings" + end + + vars = { "PWD" => path } + options = { chdir: path } + + @cmd_output = "" + @cmd_status = 0 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read + @cmd_status = wait_thr.value.exitstatus + end + + [@cmd_output, @cmd_status] + end + end + end +end diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb new file mode 100644 index 0000000000000000000000000000000000000000..37ef683674257351c35a48e7bdafbb0ad185453a --- /dev/null +++ b/lib/gitlab/git/ref.rb @@ -0,0 +1,49 @@ +module Gitlab + module Git + class Ref + include Gitlab::Git::EncodingHelper + + # Branch or tag name + # without "refs/tags|heads" prefix + attr_reader :name + + # Target sha. + # Usually it is commit sha but in case + # when tag reference on other tag it can be tag sha + attr_reader :target + + # Dereferenced target + # Commit object to which the Ref points to + attr_reader :dereferenced_target + + # Extract branch name from full ref path + # + # Ex. + # Ref.extract_branch_name('refs/heads/master') #=> 'master' + def self.extract_branch_name(str) + str.gsub(/\Arefs\/heads\//, '') + end + + def self.dereference_object(object) + object = object.target while object.is_a?(Rugged::Tag::Annotation) + + object + end + + def initialize(repository, name, target) + encode! name + @name = name.gsub(/\Arefs\/(tags|heads)\//, '') + @dereferenced_target = Gitlab::Git::Commit.find(repository, target) + @target = if target.respond_to?(:oid) + target.oid + elsif target.respond_to?(:name) + target.name + elsif target.is_a? String + target + else + nil + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb new file mode 100644 index 0000000000000000000000000000000000000000..7068e68a8553df04f91b59cbb9053e0cc8b40463 --- /dev/null +++ b/lib/gitlab/git/repository.rb @@ -0,0 +1,1251 @@ +# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object +require 'forwardable' +require 'tempfile' +require 'forwardable' +require "rubygems/package" + +module Gitlab + module Git + class Repository + extend Forwardable + include Gitlab::Git::Popen + + SEARCH_CONTEXT_LINES = 3 + + class NoRepository < StandardError; end + class InvalidBlobName < StandardError; end + class InvalidRef < StandardError; end + + # Full path to repo + attr_reader :path + + # Directory name of repo + attr_reader :name + + # Rugged repo object + attr_reader :rugged + + # 'path' must be the path to a _bare_ git repository, e.g. + # /path/to/my-repo.git + def initialize(path) + @path = path + @name = path.split("/").last + @attributes = Gitlab::Git::Attributes.new(path) + end + + # Default branch in the repository + def root_ref + @root_ref ||= discover_default_branch + end + + # Alias to old method for compatibility + def raw + rugged + end + + def rugged + @rugged ||= Rugged::Repository.new(path) + rescue Rugged::RepositoryError, Rugged::OSError + raise NoRepository.new('no repository for such path') + end + + # Returns an Array of branch names + # sorted by name ASC + def branch_names + branches.map(&:name) + end + + # Returns an Array of Branches + def branches + rugged.branches.map do |rugged_ref| + begin + Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) + rescue Rugged::ReferenceError + # Omit invalid branch + end + end.compact.sort_by(&:name) + end + + def reload_rugged + @rugged = nil + end + + # Directly find a branch with a simple name (e.g. master) + # + # force_reload causes a new Rugged repository to be instantiated + # + # This is to work around a bug in libgit2 that causes in-memory refs to + # be stale/invalid when packed-refs is changed. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333 + def find_branch(name, force_reload = false) + reload_rugged if force_reload + + rugged_ref = rugged.branches[name] + Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref + end + + def local_branches + rugged.branches.each(:local).map do |branch| + Gitlab::Git::Branch.new(self, branch.name, branch.target) + end + end + + # Returns the number of valid branches + def branch_count + rugged.branches.count do |ref| + begin + ref.name && ref.target # ensures the branch is valid + + true + rescue Rugged::ReferenceError + false + end + end + end + + # Returns an Array of tag names + def tag_names + rugged.tags.map { |t| t.name } + end + + # Returns an Array of Tags + def tags + rugged.references.each("refs/tags/*").map do |ref| + message = nil + + if ref.target.is_a?(Rugged::Tag::Annotation) + tag_message = ref.target.message + + if tag_message.respond_to?(:chomp) + message = tag_message.chomp + end + end + + Gitlab::Git::Tag.new(self, ref.name, ref.target, message) + end.sort_by(&:name) + end + + # Returns true if the given tag exists + # + # name - The name of the tag as a String. + def tag_exists?(name) + !!rugged.tags[name] + end + + # Returns true if the given branch exists + # + # name - The name of the branch as a String. + def branch_exists?(name) + rugged.branches.exists?(name) + + # If the branch name is invalid (e.g. ".foo") Rugged will raise an error. + # Whatever code calls this method shouldn't have to deal with that so + # instead we just return `false` (which is true since a branch doesn't + # exist when it has an invalid name). + rescue Rugged::ReferenceError + false + end + + # Returns an Array of branch and tag names + def ref_names + branch_names + tag_names + end + + # Deprecated. Will be removed in 5.2 + def heads + rugged.references.each("refs/heads/*").map do |head| + Gitlab::Git::Ref.new(self, head.name, head.target) + end.sort_by(&:name) + end + + def has_commits? + !empty? + end + + def empty? + rugged.empty? + end + + def bare? + rugged.bare? + end + + def repo_exists? + !!rugged + end + + # Discovers the default branch based on the repository's available branches + # + # - If no branches are present, returns nil + # - If one branch is present, returns its name + # - If two or more branches are present, returns current HEAD or master or first branch + def discover_default_branch + names = branch_names + + return if names.empty? + + return names[0] if names.length == 1 + + if rugged_head + extracted_name = Ref.extract_branch_name(rugged_head.name) + + return extracted_name if names.include?(extracted_name) + end + + if names.include?('master') + 'master' + else + names[0] + end + end + + def rugged_head + rugged.head + rescue Rugged::ReferenceError + nil + end + + def archive_metadata(ref, storage_path, format = "tar.gz") + ref ||= root_ref + commit = Gitlab::Git::Commit.find(self, ref) + return {} if commit.nil? + + project_name = self.name.chomp('.git') + prefix = "#{project_name}-#{ref}-#{commit.id}" + + { + 'RepoPath' => path, + 'ArchivePrefix' => prefix, + 'ArchivePath' => archive_file_path(prefix, storage_path, format), + 'CommitId' => commit.id, + } + end + + def archive_file_path(name, storage_path, format = "tar.gz") + # Build file path + return nil unless name + + extension = + case format + when "tar.bz2", "tbz", "tbz2", "tb2", "bz2" + "tar.bz2" + when "tar" + "tar" + when "zip" + "zip" + else + # everything else should fall back to tar.gz + "tar.gz" + end + + file_name = "#{name}.#{extension}" + File.join(storage_path, self.name, file_name) + end + + # Return repo size in megabytes + def size + size = popen(%w(du -sk), path).first.strip.to_i + (size.to_f / 1024).round(2) + end + + # Returns an array of BlobSnippets for files at the specified +ref+ that + # contain the +query+ string. + def search_files(query, ref = nil) + greps = [] + ref ||= root_ref + + populated_index(ref).each do |entry| + # Discard submodules + next if submodule?(entry) + + blob = Gitlab::Git::Blob.raw(self, entry[:oid]) + + # Skip binary files + next if blob.data.encoding == Encoding::ASCII_8BIT + + blob.load_all_data!(self) + greps += build_greps(blob.data, query, ref, entry[:path]) + end + + greps + end + + # Use the Rugged Walker API to build an array of commits. + # + # Usage. + # repo.log( + # ref: 'master', + # path: 'app/models', + # limit: 10, + # offset: 5, + # after: Time.new(2016, 4, 21, 14, 32, 10) + # ) + # + def log(options) + default_options = { + limit: 10, + offset: 0, + path: nil, + follow: false, + skip_merges: false, + disable_walk: false, + after: nil, + before: nil + } + + options = default_options.merge(options) + options[:limit] ||= 0 + options[:offset] ||= 0 + actual_ref = options[:ref] || root_ref + begin + sha = sha_from_ref(actual_ref) + rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError + # Return an empty array if the ref wasn't found + return [] + end + + if log_using_shell?(options) + log_by_shell(sha, options) + else + log_by_walk(sha, options) + end + end + + def log_using_shell?(options) + options[:path].present? || + options[:disable_walk] || + options[:skip_merges] || + options[:after] || + options[:before] + end + + def log_by_walk(sha, options) + walk_options = { + show: sha, + sort: Rugged::SORT_DATE, + limit: options[:limit], + offset: options[:offset] + } + Rugged::Walker.walk(rugged, walk_options).to_a + end + + def log_by_shell(sha, options) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log) + cmd += %W(-n #{options[:limit].to_i}) + cmd += %w(--format=%H) + cmd += %W(--skip=#{options[:offset].to_i}) + cmd += %w(--follow) if options[:follow] + cmd += %w(--no-merges) if options[:skip_merges] + cmd += %W(--after=#{options[:after].iso8601}) if options[:after] + cmd += %W(--before=#{options[:before].iso8601}) if options[:before] + cmd += [sha] + cmd += %W(-- #{options[:path]}) if options[:path].present? + + raw_output = IO.popen(cmd) {|io| io.read } + + log = raw_output.lines.map do |c| + Rugged::Commit.new(rugged, c.strip) + end + + log.is_a?(Array) ? log : [] + end + + def sha_from_ref(ref) + rev_parse_target(ref).oid + end + + # Return the object that +revspec+ points to. If +revspec+ is an + # annotated tag, then return the tag's target instead. + def rev_parse_target(revspec) + obj = rugged.rev_parse(revspec) + Ref.dereference_object(obj) + end + + # Return a collection of Rugged::Commits between the two revspec arguments. + # See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for + # a detailed list of valid arguments. + def commits_between(from, to) + walker = Rugged::Walker.new(rugged) + walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE) + + sha_from = sha_from_ref(from) + sha_to = sha_from_ref(to) + + walker.push(sha_to) + walker.hide(sha_from) + + commits = walker.to_a + walker.reset + + commits + end + + # Counts the amount of commits between `from` and `to`. + def count_commits_between(from, to) + commits_between(from, to).size + end + + # Returns the SHA of the most recent common ancestor of +from+ and +to+ + def merge_base_commit(from, to) + rugged.merge_base(from, to) + end + + # Return an array of Diff objects that represent the diff + # between +from+ and +to+. See Diff::filter_diff_options for the allowed + # diff options. The +options+ hash can also include :break_rewrites to + # split larger rewrites into delete/add pairs. + def diff(from, to, options = {}, *paths) + Gitlab::Git::DiffCollection.new(diff_patches(from, to, options, *paths), options) + end + + # Returns commits collection + # + # Ex. + # repo.find_commits( + # ref: 'master', + # max_count: 10, + # skip: 5, + # order: :date + # ) + # + # +options+ is a Hash of optional arguments to git + # :ref is the ref from which to begin (SHA1 or name) + # :contains is the commit contained by the refs from which to begin (SHA1 or name) + # :max_count is the maximum number of commits to fetch + # :skip is the number of commits to skip + # :order is the commits order and allowed value is :date(default) or :topo + # + def find_commits(options = {}) + actual_options = options.dup + + allowed_options = [:ref, :max_count, :skip, :contains, :order] + + actual_options.keep_if do |key| + allowed_options.include?(key) + end + + default_options = { skip: 0 } + actual_options = default_options.merge(actual_options) + + walker = Rugged::Walker.new(rugged) + + if actual_options[:ref] + walker.push(rugged.rev_parse_oid(actual_options[:ref])) + elsif actual_options[:contains] + branches_contains(actual_options[:contains]).each do |branch| + walker.push(branch.target_id) + end + else + rugged.references.each("refs/heads/*") do |ref| + walker.push(ref.target_id) + end + end + + if actual_options[:order] == :topo + walker.sorting(Rugged::SORT_TOPO) + else + walker.sorting(Rugged::SORT_DATE) + end + + commits = [] + offset = actual_options[:skip] + limit = actual_options[:max_count] + walker.each(offset: offset, limit: limit) do |commit| + gitlab_commit = Gitlab::Git::Commit.decorate(commit) + commits.push(gitlab_commit) + end + + walker.reset + + commits + rescue Rugged::OdbError + [] + end + + # Returns branch names collection that contains the special commit(SHA1 + # or name) + # + # Ex. + # repo.branch_names_contains('master') + # + def branch_names_contains(commit) + branches_contains(commit).map { |c| c.name } + end + + # Returns branch collection that contains the special commit(SHA1 or name) + # + # Ex. + # repo.branch_names_contains('master') + # + def branches_contains(commit) + commit_obj = rugged.rev_parse(commit) + parent = commit_obj.parents.first unless commit_obj.parents.empty? + + walker = Rugged::Walker.new(rugged) + + rugged.branches.select do |branch| + walker.push(branch.target_id) + walker.hide(parent) if parent + result = walker.any? { |c| c.oid == commit_obj.oid } + walker.reset + + result + end + end + + # Get refs hash which key is SHA1 + # and value is a Rugged::Reference + def refs_hash + # Initialize only when first call + if @refs_hash.nil? + @refs_hash = Hash.new { |h, k| h[k] = [] } + + rugged.references.each do |r| + # Symbolic/remote references may not have an OID; skip over them + target_oid = r.target.try(:oid) + if target_oid + sha = rev_parse_target(target_oid).oid + @refs_hash[sha] << r + end + end + end + @refs_hash + end + + # Lookup for rugged object by oid or ref name + def lookup(oid_or_ref_name) + rugged.rev_parse(oid_or_ref_name) + end + + # Return hash with submodules info for this repository + # + # Ex. + # { + # "rack" => { + # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320", + # "path" => "rack", + # "url" => "git://github.com/chneukirchen/rack.git" + # }, + # "encoding" => { + # "id" => .... + # } + # } + # + def submodules(ref) + commit = rev_parse_target(ref) + return {} unless commit + + begin + content = blob_content(commit, ".gitmodules") + rescue InvalidBlobName + return {} + end + + parse_gitmodules(commit, content) + end + + # Return total commits count accessible from passed ref + def commit_count(ref) + walker = Rugged::Walker.new(rugged) + walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE) + oid = rugged.rev_parse_oid(ref) + walker.push(oid) + walker.count + end + + # Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or + # tag name or a commit SHA. Valid +reset_type+ values are: + # + # [:soft] + # the head will be moved to the commit. + # [:mixed] + # will trigger a +:soft+ reset, plus the index will be replaced + # with the content of the commit tree. + # [:hard] + # will trigger a +:mixed+ reset and the working directory will be + # replaced with the content of the index. (Untracked and ignored files + # will be left alone) + def reset(ref, reset_type) + rugged.reset(ref, reset_type) + end + + # Mimic the `git clean` command and recursively delete untracked files. + # Valid keys that can be passed in the +options+ hash are: + # + # :d - Remove untracked directories + # :f - Remove untracked directories that are managed by a different + # repository + # :x - Remove ignored files + # + # The value in +options+ must evaluate to true for an option to take + # effect. + # + # Examples: + # + # repo.clean(d: true, f: true) # Enable the -d and -f options + # + # repo.clean(d: false, x: true) # -x is enabled, -d is not + def clean(options = {}) + strategies = [:remove_untracked] + strategies.push(:force) if options[:f] + strategies.push(:remove_ignored) if options[:x] + + # TODO: implement this method + end + + # Check out the specified ref. Valid options are: + # + # :b - Create a new branch at +start_point+ and set HEAD to the new + # branch. + # + # * These options are passed to the Rugged::Repository#checkout method: + # + # :progress :: + # A callback that will be executed for checkout progress notifications. + # Up to 3 parameters are passed on each execution: + # + # - The path to the last updated file (or +nil+ on the very first + # invocation). + # - The number of completed checkout steps. + # - The number of total checkout steps to be performed. + # + # :notify :: + # A callback that will be executed for each checkout notification + # types specified with +:notify_flags+. Up to 5 parameters are passed + # on each execution: + # + # - An array containing the +:notify_flags+ that caused the callback + # execution. + # - The path of the current file. + # - A hash describing the baseline blob (or +nil+ if it does not + # exist). + # - A hash describing the target blob (or +nil+ if it does not exist). + # - A hash describing the workdir blob (or +nil+ if it does not + # exist). + # + # :strategy :: + # A single symbol or an array of symbols representing the strategies + # to use when performing the checkout. Possible values are: + # + # :none :: + # Perform a dry run (default). + # + # :safe :: + # Allow safe updates that cannot overwrite uncommitted data. + # + # :safe_create :: + # Allow safe updates plus creation of missing files. + # + # :force :: + # Allow all updates to force working directory to look like index. + # + # :allow_conflicts :: + # Allow checkout to make safe updates even if conflicts are found. + # + # :remove_untracked :: + # Remove untracked files not in index (that are not ignored). + # + # :remove_ignored :: + # Remove ignored files not in index. + # + # :update_only :: + # Only update existing files, don't create new ones. + # + # :dont_update_index :: + # Normally checkout updates index entries as it goes; this stops + # that. + # + # :no_refresh :: + # Don't refresh index/config/etc before doing checkout. + # + # :disable_pathspec_match :: + # Treat pathspec as simple list of exact match file paths. + # + # :skip_locked_directories :: + # Ignore directories in use, they will be left empty. + # + # :skip_unmerged :: + # Allow checkout to skip unmerged files (NOT IMPLEMENTED). + # + # :use_ours :: + # For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED). + # + # :use_theirs :: + # For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED). + # + # :update_submodules :: + # Recursively checkout submodules with same options (NOT + # IMPLEMENTED). + # + # :update_submodules_if_changed :: + # Recursively checkout submodules if HEAD moved in super repo (NOT + # IMPLEMENTED). + # + # :disable_filters :: + # If +true+, filters like CRLF line conversion will be disabled. + # + # :dir_mode :: + # Mode for newly created directories. Default: +0755+. + # + # :file_mode :: + # Mode for newly created files. Default: +0755+ or +0644+. + # + # :file_open_flags :: + # Mode for opening files. Default: + # <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>. + # + # :notify_flags :: + # A single symbol or an array of symbols representing the cases in + # which the +:notify+ callback should be invoked. Possible values are: + # + # :none :: + # Do not invoke the +:notify+ callback (default). + # + # :conflict :: + # Invoke the callback for conflicting paths. + # + # :dirty :: + # Invoke the callback for "dirty" files, i.e. those that do not need + # an update but no longer match the baseline. + # + # :updated :: + # Invoke the callback for any file that was changed. + # + # :untracked :: + # Invoke the callback for untracked files. + # + # :ignored :: + # Invoke the callback for ignored files. + # + # :all :: + # Invoke the callback for all these cases. + # + # :paths :: + # A glob string or an array of glob strings specifying which paths + # should be taken into account for the checkout operation. +nil+ will + # match all files. Default: +nil+. + # + # :baseline :: + # A Rugged::Tree that represents the current, expected contents of the + # workdir. Default: +HEAD+. + # + # :target_directory :: + # A path to an alternative workdir directory in which the checkout + # should be performed. + def checkout(ref, options = {}, start_point = "HEAD") + if options[:b] + rugged.branches.create(ref, start_point) + options.delete(:b) + end + default_options = { strategy: [:recreate_missing, :safe] } + rugged.checkout(ref, default_options.merge(options)) + end + + # Delete the specified branch from the repository + def delete_branch(branch_name) + rugged.branches.delete(branch_name) + end + + # Create a new branch named **ref+ based on **stat_point+, HEAD by default + # + # Examples: + # create_branch("feature") + # create_branch("other-feature", "master") + def create_branch(ref, start_point = "HEAD") + rugged_ref = rugged.branches.create(ref, start_point) + Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) + rescue Rugged::ReferenceError => e + raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/ + raise InvalidRef.new("Invalid reference #{start_point}") + end + + # Return an array of this repository's remote names + def remote_names + rugged.remotes.each_name.to_a + end + + # Delete the specified remote from this repository. + def remote_delete(remote_name) + rugged.remotes.delete(remote_name) + end + + # Add a new remote to this repository. Returns a Rugged::Remote object + def remote_add(remote_name, url) + rugged.remotes.create(remote_name, url) + end + + # Update the specified remote using the values in the +options+ hash + # + # Example + # repo.update_remote("origin", url: "path/to/repo") + def remote_update(remote_name, options = {}) + # TODO: Implement other remote options + rugged.remotes.set_url(remote_name, options[:url]) if options[:url] + end + + # Fetch the specified remote + def fetch(remote_name) + rugged.remotes[remote_name].fetch + end + + # Push +*refspecs+ to the remote identified by +remote_name+. + def push(remote_name, *refspecs) + rugged.remotes[remote_name].push(refspecs) + end + + # Merge the +source_name+ branch into the +target_name+ branch. This is + # equivalent to `git merge --no_ff +source_name+`, since a merge commit + # is always created. + def merge(source_name, target_name, options = {}) + our_commit = rugged.branches[target_name].target + their_commit = rugged.branches[source_name].target + + raise "Invalid merge target" if our_commit.nil? + raise "Invalid merge source" if their_commit.nil? + + merge_index = rugged.merge_commits(our_commit, their_commit) + return false if merge_index.conflicts? + + actual_options = options.merge( + parents: [our_commit, their_commit], + tree: merge_index.write_tree(rugged), + update_ref: "refs/heads/#{target_name}" + ) + Rugged::Commit.create(rugged, actual_options) + end + + def commits_since(from_date) + walker = Rugged::Walker.new(rugged) + walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE) + + rugged.references.each("refs/heads/*") do |ref| + walker.push(ref.target_id) + end + + commits = [] + walker.each do |commit| + break if commit.author[:time].to_date < from_date + commits.push(commit) + end + + commits + end + + AUTOCRLF_VALUES = { + "true" => true, + "false" => false, + "input" => :input + }.freeze + + def autocrlf + AUTOCRLF_VALUES[rugged.config['core.autocrlf']] + end + + def autocrlf=(value) + rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value] + end + + # Create a new directory with a .gitkeep file. Creates + # all required nested directories (i.e. mkdir -p behavior) + # + # options should contain next structure: + # author: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # committer: { + # email: 'user@example.com', + # name: 'Test User', + # time: Time.now + # }, + # commit: { + # message: 'Wow such commit', + # branch: 'master', + # update_ref: false + # } + def mkdir(path, options = {}) + # Check if this directory exists; if it does, then don't bother + # adding .gitkeep file. + ref = options[:commit][:branch] + path = Gitlab::Git::PathHelper.normalize_path(path).to_s + rugged_ref = rugged.ref(ref) + + raise InvalidRef.new("Invalid ref") if rugged_ref.nil? + + target_commit = rugged_ref.target + + raise InvalidRef.new("Invalid target commit") if target_commit.nil? + + entry = tree_entry(target_commit, path) + + if entry + if entry[:type] == :blob + raise InvalidBlobName.new("Directory already exists as a file") + else + raise InvalidBlobName.new("Directory already exists") + end + end + + options[:file] = { + content: '', + path: "#{path}/.gitkeep", + update: true + } + + Gitlab::Git::Blob.commit(self, options) + end + + # Returns result like "git ls-files" , recursive and full file path + # + # Ex. + # repo.ls_files('master') + # + def ls_files(ref) + actual_ref = ref || root_ref + + begin + sha_from_ref(actual_ref) + rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError + # Return an empty array if the ref wasn't found + return [] + end + + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) + cmd += %w(-r) + cmd += %w(--full-tree) + cmd += %w(--full-name) + cmd += %W(-- #{actual_ref}) + + raw_output = IO.popen(cmd, &:read).split("\n").map do |f| + stuff, path = f.split("\t") + _mode, type, _sha = stuff.split(" ") + path if type == "blob" + # Contain only blob type + end + + raw_output.compact + end + + def copy_gitattributes(ref) + begin + commit = lookup(ref) + rescue Rugged::ReferenceError + raise InvalidRef.new("Ref #{ref} is invalid") + end + + # Create the paths + info_dir_path = File.join(path, 'info') + info_attributes_path = File.join(info_dir_path, 'attributes') + + begin + # Retrieve the contents of the blob + gitattributes_content = blob_content(commit, '.gitattributes') + rescue InvalidBlobName + # No .gitattributes found. Should now remove any info/attributes and return + File.delete(info_attributes_path) if File.exist?(info_attributes_path) + return + end + + # Create the info directory if needed + Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path) + + # Write the contents of the .gitattributes file to info/attributes + # Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8 + File.open(info_attributes_path, "wb") do |file| + file.write(gitattributes_content) + end + end + + # Checks if the blob should be diffable according to its attributes + def diffable?(blob) + attributes(blob.path).fetch('diff') { blob.text? } + end + + # Returns the Git attributes for the given file path. + # + # See `Gitlab::Git::Attributes` for more information. + def attributes(path) + @attributes.attributes(path) + end + + private + + # Get the content of a blob for a given commit. If the blob is a commit + # (for submodules) then return the blob's OID. + def blob_content(commit, blob_name) + blob_entry = tree_entry(commit, blob_name) + + unless blob_entry + raise InvalidBlobName.new("Invalid blob name: #{blob_name}") + end + + case blob_entry[:type] + when :commit + blob_entry[:oid] + when :tree + raise InvalidBlobName.new("#{blob_name} is a tree, not a blob") + when :blob + rugged.lookup(blob_entry[:oid]).content + end + end + + # Parses the contents of a .gitmodules file and returns a hash of + # submodule information. + def parse_gitmodules(commit, content) + results = {} + + current = "" + content.split("\n").each do |txt| + if txt =~ /^\s*\[/ + current = txt.match(/(?<=").*(?=")/)[0] + results[current] = {} + else + next unless results[current] + match_data = txt.match(/(\w+)\s*=\s*(.*)/) + next unless match_data + target = match_data[2].chomp + results[current][match_data[1]] = target + + if match_data[1] == "path" + begin + results[current]["id"] = blob_content(commit, target) + rescue InvalidBlobName + results.delete(current) + end + end + end + end + + results + end + + # Returns true if +commit+ introduced changes to +path+, using commit + # trees to make that determination. Uses the history simplification + # rules that `git log` uses by default, where a commit is omitted if it + # is TREESAME to any parent. + # + # If the +follow+ option is true and the file specified by +path+ was + # renamed, then the path value is set to the old path. + def commit_touches_path?(commit, path, follow, walker) + entry = tree_entry(commit, path) + + if commit.parents.empty? + # This is the root commit, return true if it has +path+ in its tree + return !entry.nil? + end + + num_treesame = 0 + commit.parents.each do |parent| + parent_entry = tree_entry(parent, path) + + # Only follow the first TREESAME parent for merge commits + if num_treesame > 0 + walker.hide(parent) + next + end + + if entry.nil? && parent_entry.nil? + num_treesame += 1 + elsif entry && parent_entry && entry[:oid] == parent_entry[:oid] + num_treesame += 1 + end + end + + case num_treesame + when 0 + detect_rename(commit, commit.parents.first, path) if follow + true + else false + end + end + + # Find the entry for +path+ in the tree for +commit+ + def tree_entry(commit, path) + pathname = Pathname.new(path) + first = true + tmp_entry = nil + + pathname.each_filename do |dir| + if first + tmp_entry = commit.tree[dir] + first = false + elsif tmp_entry.nil? + return nil + else + tmp_entry = rugged.lookup(tmp_entry[:oid]) + return nil unless tmp_entry.type == :tree + tmp_entry = tmp_entry[dir] + end + end + + tmp_entry + end + + # Compare +commit+ and +parent+ for +path+. If +path+ is a file and was + # renamed in +commit+, then set +path+ to the old filename. + def detect_rename(commit, parent, path) + diff = parent.diff(commit, paths: [path], disable_pathspec_match: true) + + # If +path+ is a filename, not a directory, then we should only have + # one delta. We don't need to follow renames for directories. + return nil if diff.each_delta.count > 1 + + delta = diff.each_delta.first + if delta.added? + full_diff = parent.diff(commit) + full_diff.find_similar! + + full_diff.each_delta do |full_delta| + if full_delta.renamed? && path == full_delta.new_file[:path] + # Look for the old path in ancestors + path.replace(full_delta.old_file[:path]) + end + end + end + end + + def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n)) + git_archive_cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} archive) + + # Put files into a directory before archiving + prefix = "#{archive_name(treeish)}/" + git_archive_cmd << "--prefix=#{prefix}" + + # Format defaults to tar + git_archive_cmd << "--format=#{format}" if format + + git_archive_cmd += %W(-- #{treeish}) + + open(filename, 'w') do |file| + # Create a pipe to act as the '|' in 'git archive ... | gzip' + pipe_rd, pipe_wr = IO.pipe + + # Get the compression process ready to accept data from the read end + # of the pipe + compress_pid = spawn(*nice(compress_cmd), in: pipe_rd, out: file) + # The read end belongs to the compression process now; we should + # close our file descriptor for it. + pipe_rd.close + + # Start 'git archive' and tell it to write into the write end of the + # pipe. + git_archive_pid = spawn(*nice(git_archive_cmd), out: pipe_wr) + # The write end belongs to 'git archive' now; close it. + pipe_wr.close + + # When 'git archive' and the compression process are finished, we are + # done. + Process.waitpid(git_archive_pid) + raise "#{git_archive_cmd.join(' ')} failed" unless $?.success? + Process.waitpid(compress_pid) + raise "#{compress_cmd.join(' ')} failed" unless $?.success? + end + end + + def nice(cmd) + nice_cmd = %w(nice -n 20) + unless unsupported_platform? + nice_cmd += %w(ionice -c 2 -n 7) + end + nice_cmd + cmd + end + + def unsupported_platform? + %w[darwin freebsd solaris].map { |platform| RUBY_PLATFORM.include?(platform) }.any? + end + + # Returns true if the index entry has the special file mode that denotes + # a submodule. + def submodule?(index_entry) + index_entry[:mode] == 57344 + end + + # Return a Rugged::Index that has read from the tree at +ref_name+ + def populated_index(ref_name) + commit = rev_parse_target(ref_name) + index = rugged.index + index.read_tree(commit.tree) + index + end + + # Return an array of BlobSnippets for lines in +file_contents+ that match + # +query+ + def build_greps(file_contents, query, ref, filename) + # The file_contents string is potentially huge so we make sure to loop + # through it one line at a time. This gives Ruby the chance to GC lines + # we are not interested in. + # + # We need to do a little extra work because we are not looking for just + # the lines that matches the query, but also for the context + # (surrounding lines). We will use Enumerable#each_cons to efficiently + # loop through the lines while keeping surrounding lines on hand. + # + # First, we turn "foo\nbar\nbaz" into + # [ + # [nil, -3], [nil, -2], [nil, -1], + # ['foo', 0], ['bar', 1], ['baz', 3], + # [nil, 4], [nil, 5], [nil, 6] + # ] + lines_with_index = Enumerator.new do |yielder| + # Yield fake 'before' lines for the first line of file_contents + (-SEARCH_CONTEXT_LINES..-1).each do |i| + yielder.yield [nil, i] + end + + # Yield the actual file contents + count = 0 + file_contents.each_line do |line| + line.chomp! + yielder.yield [line, count] + count += 1 + end + + # Yield fake 'after' lines for the last line of file_contents + (count + 1..count + SEARCH_CONTEXT_LINES).each do |i| + yielder.yield [nil, i] + end + end + + greps = [] + + # Loop through consecutive blocks of lines with indexes + lines_with_index.each_cons(2 * SEARCH_CONTEXT_LINES + 1) do |line_block| + # Get the 'middle' line and index from the block + line, _ = line_block[SEARCH_CONTEXT_LINES] + + next unless line && line.match(/#{Regexp.escape(query)}/i) + + # Yay, 'line' contains a match! + # Get an array with just the context lines (no indexes) + match_with_context = line_block.map(&:first) + # Remove 'nil' lines in case we are close to the first or last line + match_with_context.compact! + + # Get the line number (1-indexed) of the first context line + first_context_line_number = line_block[0][1] + 1 + + greps << Gitlab::Git::BlobSnippet.new( + ref, + match_with_context, + first_context_line_number, + filename + ) + end + + greps + end + + # Return the Rugged patches for the diff between +from+ and +to+. + def diff_patches(from, to, options = {}, *paths) + options ||= {} + break_rewrites = options[:break_rewrites] + actual_options = Gitlab::Git::Diff.filter_diff_options(options.merge(paths: paths)) + + diff = rugged.diff(from, to, actual_options) + diff.find_similar!(break_rewrites: break_rewrites) + diff.each_patch + end + end + end +end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb new file mode 100644 index 0000000000000000000000000000000000000000..79dd0cf7df28f83b89b887bc57fafda3d5345377 --- /dev/null +++ b/lib/gitlab/git/rev_list.rb @@ -0,0 +1,42 @@ +module Gitlab + module Git + class RevList + attr_reader :project, :env + + ALLOWED_VARIABLES = %w[GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES].freeze + + def initialize(oldrev, newrev, project:, env: nil) + @project = project + @env = env.presence || {} + @args = [Gitlab.config.git.bin_path, + "--git-dir=#{project.repository.path_to_repo}", + "rev-list", + "--max-count=1", + oldrev, + "^#{newrev}"] + end + + def execute + Gitlab::Popen.popen(@args, nil, parse_environment_variables) + end + + def valid? + environment_variables.all? do |(name, value)| + value.to_s.start_with?(project.repository.path_to_repo) + end + end + + private + + def parse_environment_variables + return {} unless valid? + + environment_variables + end + + def environment_variables + @environment_variables ||= env.slice(*ALLOWED_VARIABLES).compact + end + end + end +end diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb new file mode 100644 index 0000000000000000000000000000000000000000..b5342c3d310a2798a036575386f0692f99a04612 --- /dev/null +++ b/lib/gitlab/git/tag.rb @@ -0,0 +1,17 @@ +module Gitlab + module Git + class Tag < Ref + attr_reader :object_sha + + def initialize(repository, name, target, message = nil) + super(repository, name, target) + + @message = message + end + + def message + encode! @message + end + end + end +end diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb new file mode 100644 index 0000000000000000000000000000000000000000..f7450e8b58fa17a0ae95af70d9f81998f4d6dcf6 --- /dev/null +++ b/lib/gitlab/git/tree.rb @@ -0,0 +1,104 @@ +module Gitlab + module Git + class Tree + include Gitlab::Git::EncodingHelper + + attr_accessor :id, :root_id, :name, :path, :type, + :mode, :commit_id, :submodule_url + + class << self + # Get list of tree objects + # for repository based on commit sha and path + # Uses rugged for raw objects + def where(repository, sha, path = nil) + path = nil if path == '' || path == '/' + + commit = repository.lookup(sha) + root_tree = commit.tree + + tree = if path + id = find_id_by_path(repository, root_tree.oid, path) + if id + repository.lookup(id) + else + [] + end + else + root_tree + end + + tree.map do |entry| + new( + id: entry[:oid], + root_id: root_tree.oid, + name: entry[:name], + type: entry[:type], + mode: entry[:filemode], + path: path ? File.join(path, entry[:name]) : entry[:name], + commit_id: sha, + ) + end + end + + # Recursive search of tree id for path + # + # Ex. + # blog/ # oid: 1a + # app/ # oid: 2a + # models/ # oid: 3a + # views/ # oid: 4a + # + # + # Tree.find_id_by_path(repo, '1a', 'app/models') # => '3a' + # + def find_id_by_path(repository, root_id, path) + root_tree = repository.lookup(root_id) + path_arr = path.split('/') + + entry = root_tree.find do |entry| + entry[:name] == path_arr[0] && entry[:type] == :tree + end + + return nil unless entry + + if path_arr.size > 1 + path_arr.shift + find_id_by_path(repository, entry[:oid], path_arr.join('/')) + else + entry[:oid] + end + end + end + + def initialize(options) + %w(id root_id name path type mode commit_id).each do |key| + self.send("#{key}=", options[key.to_sym]) + end + end + + def name + encode! @name + end + + def dir? + type == :tree + end + + def file? + type == :blob + end + + def submodule? + type == :commit + end + + def readme? + name =~ /^readme/i + end + + def contributing? + name =~ /^contributing/i + end + end + end +end diff --git a/lib/gitlab/git/util.rb b/lib/gitlab/git/util.rb new file mode 100644 index 0000000000000000000000000000000000000000..7973da2e8f8f9fd59e5151d0641b14bb58143b5f --- /dev/null +++ b/lib/gitlab/git/util.rb @@ -0,0 +1,18 @@ +module Gitlab + module Git + module Util + LINE_SEP = "\n".freeze + + def self.count_lines(string) + case string[-1] + when nil + 0 + when LINE_SEP + string.count(LINE_SEP) + else + string.count(LINE_SEP) + 1 + end + end + end + end +end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index db07b7c5fcc5756dd160e44f7b3cee02c41a96ab..7e1484613f2895f6be896eec1e487d83a8aaf7fc 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -7,7 +7,8 @@ module Gitlab ERROR_MESSAGES = { upload: 'You are not allowed to upload code for this project.', download: 'You are not allowed to download code from this project.', - deploy_key: 'Deploy keys are not allowed to push code.', + deploy_key_upload: + 'This deploy key does not have write access to this project.', no_repo: 'A repository for this project does not exist yet.' } @@ -17,12 +18,13 @@ module Gitlab attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities - def initialize(actor, project, protocol, authentication_abilities:) + def initialize(actor, project, protocol, authentication_abilities:, env: {}) @actor = actor @project = project @protocol = protocol @authentication_abilities = authentication_abilities @user_access = UserAccess.new(user, project: project) + @env = env end def check(cmd, changes) @@ -30,12 +32,13 @@ module Gitlab check_active_user! check_project_accessibility! check_command_existence!(cmd) + check_repository_existence! case cmd when *DOWNLOAD_COMMANDS - download_access_check + check_download_access! when *PUSH_COMMANDS - push_access_check(changes) + check_push_access!(changes) end build_status_object(true) @@ -43,32 +46,10 @@ module Gitlab build_status_object(false, ex.message) end - def download_access_check - if user - user_download_access_check - elsif deploy_key.nil? && !guest_can_downlod_code? - raise UnauthorizedError, ERROR_MESSAGES[:download] - end - end - - def push_access_check(changes) - if user - user_push_access_check(changes) - else - raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload] - end - end - - def guest_can_downlod_code? + def guest_can_download_code? Guest.can?(:download_code, project) end - def user_download_access_check - unless user_can_download_code? || build_can_download_code? - raise UnauthorizedError, ERROR_MESSAGES[:download] - end - end - def user_can_download_code? authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code) end @@ -77,35 +58,6 @@ module Gitlab authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code) end - def user_push_access_check(changes) - unless authentication_abilities.include?(:push_code) - raise UnauthorizedError, ERROR_MESSAGES[:upload] - end - - if changes.blank? - return # Allow access. - end - - unless project.repository.exists? - raise UnauthorizedError, ERROR_MESSAGES[:no_repo] - end - - changes_list = Gitlab::ChangesList.new(changes) - - # Iterate over all changes to find if user allowed all of them to be applied - changes_list.each do |change| - status = change_access_check(change) - unless status.allowed? - # If user does not have access to make at least one change - cancel all push - raise UnauthorizedError, status.message - end - end - end - - def change_access_check(change) - Checks::ChangeAccess.new(change, user_access: user_access, project: project).exec - end - def protocol_allowed? Gitlab::ProtocolAccess.allowed?(protocol) end @@ -119,6 +71,8 @@ module Gitlab end def check_active_user! + return if deploy_key? + if user && !user_access.allowed? raise UnauthorizedError, "Your account has been blocked." end @@ -136,33 +90,92 @@ module Gitlab end end - def matching_merge_request?(newrev, branch_name) - Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? + def check_repository_existence! + unless project.repository.exists? + raise UnauthorizedError, ERROR_MESSAGES[:no_repo] + end end - def deploy_key - actor if actor.is_a?(DeployKey) + def check_download_access! + return if deploy_key? + + passed = user_can_download_code? || + build_can_download_code? || + guest_can_download_code? + + unless passed + raise UnauthorizedError, ERROR_MESSAGES[:download] + end end - def deploy_key_can_read_project? + def check_push_access!(changes) if deploy_key - return true if project.public? - deploy_key.projects.include?(project) + check_deploy_key_push_access! + elsif user + check_user_push_access! else - false + raise UnauthorizedError, ERROR_MESSAGES[:upload] end + + return if changes.blank? # Allow access. + + check_change_access!(changes) end - def can_read_project? - if user - user_access.can_read_project? - elsif deploy_key - deploy_key_can_read_project? - else - Guest.can?(:read_project, project) + def check_user_push_access! + unless authentication_abilities.include?(:push_code) + raise UnauthorizedError, ERROR_MESSAGES[:upload] end end + def check_deploy_key_push_access! + unless deploy_key.can_push_to?(project) + raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload] + end + end + + def check_change_access!(changes) + changes_list = Gitlab::ChangesList.new(changes) + + # Iterate over all changes to find if user allowed all of them to be applied + changes_list.each do |change| + status = check_single_change_access(change) + unless status.allowed? + # If user does not have access to make at least one change - cancel all push + raise UnauthorizedError, status.message + end + end + end + + def check_single_change_access(change) + Checks::ChangeAccess.new( + change, + user_access: user_access, + project: project, + env: @env, + skip_authorization: deploy_key?).exec + end + + def matching_merge_request?(newrev, branch_name) + Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? + end + + def deploy_key + actor if deploy_key? + end + + def deploy_key? + actor.is_a?(DeployKey) + end + + def can_read_project? + if deploy_key + deploy_key.has_access_to?(project) + elsif user + user.can?(:read_project, project) + end || Guest.can?(:read_project, project) + end + protected def user diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index 2c06c4ff1ef0cfb7eaea2c1efcbeebb2f46009ec..67eaa5e088d87a86f7c679a535be198156d53f9d 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -1,6 +1,6 @@ module Gitlab class GitAccessWiki < GitAccess - def guest_can_downlod_code? + def guest_can_download_code? Guest.can?(:download_wiki_code, project) end @@ -8,7 +8,7 @@ module Gitlab authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code) end - def change_access_check(change) + def check_single_change_access(change) if user_access.can_do_action?(:create_wiki) build_status_object(true) else diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 6dbae64a9fe767b21a977cb48f2abc5c7d429d8a..95dba9a327bc53fdb09c07735e6ae1992e9bc1cb 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -15,6 +15,10 @@ module Gitlab end end + def url + raw_data.url || '' + end + private def gitlab_user_id(github_id) diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 85df6547a673d61b77fc5eb7af0b7f86c0f21c98..ba869faa92edea816418e04dddee24b6e560c08c 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -4,10 +4,12 @@ module Gitlab GITHUB_SAFE_REMAINING_REQUESTS = 100 GITHUB_SAFE_SLEEP_TIME = 500 - attr_reader :access_token + attr_reader :access_token, :host, :api_version - def initialize(access_token) + def initialize(access_token, host: nil, api_version: 'v3') @access_token = access_token + @host = host.to_s.sub(%r{/+\z}, '') + @api_version = api_version if access_token ::Octokit.auto_paginate = false @@ -17,7 +19,7 @@ module Gitlab def api @api ||= ::Octokit::Client.new( access_token: access_token, - api_endpoint: github_options[:site], + api_endpoint: api_endpoint, # If there is no config, we're connecting to github.com and we # should verify ssl. connection_options: { @@ -64,6 +66,14 @@ module Gitlab private + def api_endpoint + if host.present? && api_version.present? + "#{host}/api/#{api_version}" + else + github_options[:site] + end + end + def config Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" } end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 281b65bdeba2b229e4799ef0a8fc4cdb49787ce0..ec1318ab33c30a5d6118d022bd8c50002fc8731b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,7 +3,7 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :client, :errors, :project, :repo, :repo_url + attr_reader :errors, :project, :repo, :repo_url def initialize(project) @project = project @@ -11,12 +11,27 @@ module Gitlab @repo_url = project.import_url @errors = [] @labels = {} + end + + def client + return @client if defined?(@client) + unless credentials + raise Projects::ImportService::Error, + "Unable to find project import data credentials for project ID: #{@project.id}" + end - if credentials - @client = Client.new(credentials[:user]) - else - raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + opts = {} + # Gitea plan to be GitHub compliant + if project.gitea_import? + uri = URI.parse(project.import_url) + host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '') + opts = { + host: host, + api_version: 'v1' + } end + + @client = Client.new(credentials[:user], opts) end def execute @@ -35,7 +50,13 @@ module Gitlab import_comments(:issues) import_comments(:pull_requests) import_wiki - import_releases + + # Gitea doesn't have a Release API yet + # See https://github.com/go-gitea/gitea/issues/330 + unless project.gitea_import? + import_releases + end + handle_errors true @@ -44,7 +65,9 @@ module Gitlab private def credentials - @credentials ||= project.import_data.credentials if project.import_data + return @credentials if defined?(@credentials) + + @credentials = project.import_data ? project.import_data.credentials : nil end def handle_errors @@ -60,9 +83,10 @@ module Gitlab fetch_resources(:labels, repo, per_page: 100) do |labels| labels.each do |raw| begin - LabelFormatter.new(project, raw).create! + gh_label = LabelFormatter.new(project, raw) + gh_label.create! rescue => e - errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message } end end end @@ -74,9 +98,10 @@ module Gitlab fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| milestones.each do |raw| begin - MilestoneFormatter.new(project, raw).create! + gh_milestone = MilestoneFormatter.new(project, raw) + gh_milestone.create! rescue => e - errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message } end end end @@ -97,7 +122,7 @@ module Gitlab apply_labels(issuable, raw) rescue => e - errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message } end end end @@ -106,18 +131,23 @@ module Gitlab def import_pull_requests fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| - pull_request = PullRequestFormatter.new(project, raw) - next unless pull_request.valid? + gh_pull_request = PullRequestFormatter.new(project, raw) + next unless gh_pull_request.valid? begin - restore_source_branch(pull_request) unless pull_request.source_branch_exists? - restore_target_branch(pull_request) unless pull_request.target_branch_exists? + restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists? + restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists? + + merge_request = gh_pull_request.create! - pull_request.create! + # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage + if project.gitea_import? + apply_labels(merge_request, raw) + end rescue => e - errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message } + errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message } ensure - clean_up_restored_branches(pull_request) + clean_up_restored_branches(gh_pull_request) end end end @@ -233,7 +263,7 @@ module Gitlab gh_release = ReleaseFormatter.new(project, raw) gh_release.create! if gh_release.valid? rescue => e - errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message } end end end diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb new file mode 100644 index 0000000000000000000000000000000000000000..256f360efc7b79a0b0c44be9d9fc60e8b70be349 --- /dev/null +++ b/lib/gitlab/github_import/issuable_formatter.rb @@ -0,0 +1,60 @@ +module Gitlab + module GithubImport + class IssuableFormatter < BaseFormatter + def project_association + raise NotImplementedError + end + + def number + raw_data.number + end + + def find_condition + { iid: number } + end + + private + + def state + raw_data.state == 'closed' ? 'closed' : 'opened' + end + + def assigned? + raw_data.assignee.present? + end + + def assignee_id + if assigned? + gitlab_user_id(raw_data.assignee.id) + end + end + + def author + raw_data.user.login + end + + def author_id + gitlab_author_id || project.creator_id + end + + def body + raw_data.body || "" + end + + def description + if gitlab_author_id + body + else + formatter.author_line(author) + body + end + end + + def milestone + if raw_data.milestone.present? + milestone = MilestoneFormatter.new(project, raw_data.milestone) + project.milestones.find_by(milestone.find_condition) + end + end + end + end +end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 887690bcc7c841ffa333c63ca16764da5aedd5b6..6f5ac4dac0d3885f152644696dc30f85dcab0422 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -1,6 +1,6 @@ module Gitlab module GithubImport - class IssueFormatter < BaseFormatter + class IssueFormatter < IssuableFormatter def attributes { iid: number, @@ -24,59 +24,9 @@ module Gitlab :issues end - def find_condition - { iid: number } - end - - def number - raw_data.number - end - def pull_request? raw_data.pull_request.present? end - - private - - def assigned? - raw_data.assignee.present? - end - - def assignee_id - if assigned? - gitlab_user_id(raw_data.assignee.id) - end - end - - def author - raw_data.user.login - end - - def author_id - gitlab_author_id || project.creator_id - end - - def body - raw_data.body || "" - end - - def description - if gitlab_author_id - body - else - formatter.author_line(author) + body - end - end - - def milestone - if raw_data.milestone.present? - project.milestones.find_by(iid: raw_data.milestone.number) - end - end - - def state - raw_data.state == 'closed' ? 'closed' : 'opened' - end end end end diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index 401dd962521c4ee0f470d490ff448b26a5ac8dea..dd782eff059777a9d46c1a994916144e1b2328cc 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -3,7 +3,7 @@ module Gitlab class MilestoneFormatter < BaseFormatter def attributes { - iid: raw_data.number, + iid: number, project: project, title: raw_data.title, description: raw_data.description, @@ -19,7 +19,15 @@ module Gitlab end def find_condition - { iid: raw_data.number } + { iid: number } + end + + def number + if project.gitea_import? + raw_data.id + else + raw_data.number + end end private diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index a241006884522f5fabc2a102c8dbbdf72195c26c..3f635be22ba0bf7739cb5615cec2a486f290243a 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -1,14 +1,15 @@ module Gitlab module GithubImport class ProjectCreator - attr_reader :repo, :name, :namespace, :current_user, :session_data + attr_reader :repo, :name, :namespace, :current_user, :session_data, :type - def initialize(repo, name, namespace, current_user, session_data) + def initialize(repo, name, namespace, current_user, session_data, type: 'github') @repo = repo @name = name @namespace = namespace @current_user = current_user @session_data = session_data + @type = type end def execute @@ -19,7 +20,7 @@ module Gitlab description: repo.description, namespace_id: namespace.id, visibility_level: visibility_level, - import_type: "github", + import_type: type, import_source: repo.full_name, import_url: import_url, skip_wiki: skip_wiki @@ -29,7 +30,7 @@ module Gitlab private def import_url - repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@") + repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@") end def visibility_level diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index b9a227fb11a5d13040549327da0a41933e2f0f55..4ea0200e89b7f928e25ae91c84a7b93bc50b887e 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,6 +1,6 @@ module Gitlab module GithubImport - class PullRequestFormatter < BaseFormatter + class PullRequestFormatter < IssuableFormatter delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true @@ -28,14 +28,6 @@ module Gitlab :merge_requests end - def find_condition - { iid: number } - end - - def number - raw_data.number - end - def valid? source_branch.valid? && target_branch.valid? end @@ -60,57 +52,15 @@ module Gitlab end end - def url - raw_data.url - end - private - def assigned? - raw_data.assignee.present? - end - - def assignee_id - if assigned? - gitlab_user_id(raw_data.assignee.id) - end - end - - def author - raw_data.user.login - end - - def author_id - gitlab_author_id || project.creator_id - end - - def body - raw_data.body || "" - end - - def description - if gitlab_author_id - body + def state + if raw_data.state == 'closed' && raw_data.merged_at.present? + 'merged' else - formatter.author_line(author) + body + super end end - - def milestone - if raw_data.milestone.present? - project.milestones.find_by(iid: raw_data.milestone.number) - end - end - - def state - @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present? - 'merged' - elsif raw_data.state == 'closed' - 'closed' - else - 'opened' - end - end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 2c21804fe7a6328b2f1d027b0dcacc918119d753..b8a5ac907a44f9356d385fa7ed88e661e3235afa 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,9 +8,12 @@ module Gitlab gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.award_menu_url = emojis_path + gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') + gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') if current_user gon.current_user_id = current_user.id + gon.current_username = current_user.username end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index e6ecd11860999cdef08ad2c2d9b217419fa47249..08ad3274b3803f4155a8df3efa72e14deb01511f 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -6,6 +6,7 @@ project_tree: - :events - issues: - :events + - :timelogs - notes: - :author - :events @@ -27,6 +28,7 @@ project_tree: - :events - :merge_request_diff - :events + - :timelogs - label_links: - label: :priorities diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index c551321c18dbb3aeb879cdc37138e40941bddb70..cda6ddf04438f1d2923a8ec693012247ba5f87bf 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -120,7 +120,7 @@ module Gitlab members_mapper: members_mapper, user: @user, project_id: restored_project.id) - end + end.compact relation_hash_list.is_a?(Array) ? relation_array : relation_array.first end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index a0e80fccad9295c903e2fef142ebecc77251f3e7..7a649f28340f848fa1a9479767d4fb235f3ea780 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -14,7 +14,7 @@ module Gitlab priorities: :label_priorities, label: :project_label }.freeze - USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze + USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze @@ -22,7 +22,7 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze def self.create(*args) new(*args).create @@ -40,6 +40,8 @@ module Gitlab # the relation_hash, updating references with new object IDs, mapping users using # the "members_mapper" object, also updating notes if required. def create + return nil if unknown_service? + setup_models generate_imported_object @@ -99,6 +101,8 @@ module Gitlab def generate_imported_object if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes trace = @relation_hash.delete('trace') + @relation_hash.delete('token') + imported_object do |object| object.trace = trace object.commit_id = nil @@ -185,7 +189,7 @@ module Gitlab # Otherwise always create the record, skipping the extra SELECT clause. @existing_or_new_object ||= begin if EXISTING_OBJECT_CHECK.include?(@relation_name) - attribute_hash = attribute_hash_for(['events', 'priorities']) + attribute_hash = attribute_hash_for(['events']) existing_object.assign_attributes(attribute_hash) if attribute_hash.any? @@ -206,15 +210,38 @@ module Gitlab def existing_object @existing_object ||= begin - finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] - finder_hash = parsed_relation_hash.slice(*finder_attributes) - existing_object = relation_class.find_or_create_by(finder_hash) + existing_object = find_or_create_object! + # Done in two steps, as MySQL behaves differently than PostgreSQL using # the +find_or_create_by+ method and does not return the ID the second time. existing_object.update!(parsed_relation_hash) existing_object end end + + def unknown_service? + @relation_name == :services && parsed_relation_hash['type'] && + !Object.const_defined?(parsed_relation_hash['type']) + end + + def find_or_create_object! + finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] + finder_hash = parsed_relation_hash.slice(*finder_attributes) + + if label? + label = relation_class.find_or_initialize_by(finder_hash) + parsed_relation_hash.delete('priorities') if label.persisted? + + label.save! + label + else + relation_class.find_or_create_by(finder_hash) + end + end + + def label? + @relation_name.to_s.include?('label') + end end end end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 94261b7eeeded0b6e1760473f647c6e3d899a1bd..45958710c13d26410a9c1351c580a9f98387dbc9 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -7,21 +7,38 @@ module Gitlab module ImportSources extend CurrentSettings + ImportSource = Struct.new(:name, :title, :importer) + + ImportTable = [ + ImportSource.new('github', 'GitHub', Gitlab::GithubImport::Importer), + ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer), + ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer), + ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer), + ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer), + ImportSource.new('git', 'Repo by URL', nil), + ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer), + ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer) + ].freeze + class << self + def options + @options ||= Hash[ImportTable.map { |importer| [importer.title, importer.name] }] + end + def values - options.values + @values ||= ImportTable.map(&:name) end - def options - { - 'GitHub' => 'github', - 'Bitbucket' => 'bitbucket', - 'GitLab.com' => 'gitlab', - 'Google Code' => 'google_code', - 'FogBugz' => 'fogbugz', - 'Repo by URL' => 'git', - 'GitLab export' => 'gitlab_project' - } + def importer_names + @importer_names ||= ImportTable.select(&:importer).map(&:name) + end + + def importer(name) + ImportTable.find { |import_source| import_source.name == name }.importer + end + + def title(name) + options.key(name) end end end diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb new file mode 100644 index 0000000000000000000000000000000000000000..288771c1c124d0bdd67b6ed161c1877267597c5f --- /dev/null +++ b/lib/gitlab/kubernetes.rb @@ -0,0 +1,80 @@ +module Gitlab + # Helper methods to do with Kubernetes network services & resources + module Kubernetes + # This is the comand that is run to start a terminal session. Kubernetes + # expects `command=foo&command=bar, not `command[]=foo&command[]=bar` + EXEC_COMMAND = URI.encode_www_form( + ['sh', '-c', 'bash || sh'].map { |value| ['command', value] } + ) + + # Filters an array of pods (as returned by the kubernetes API) by their labels + def filter_pods(pods, labels = {}) + pods.select do |pod| + metadata = pod.fetch("metadata", {}) + pod_labels = metadata.fetch("labels", nil) + next unless pod_labels + + labels.all? { |k, v| pod_labels[k.to_s] == v } + end + end + + # Converts a pod (as returned by the kubernetes API) into a terminal + def terminals_for_pod(api_url, namespace, pod) + metadata = pod.fetch("metadata", {}) + status = pod.fetch("status", {}) + spec = pod.fetch("spec", {}) + + containers = spec["containers"] + pod_name = metadata["name"] + phase = status["phase"] + + return unless containers.present? && pod_name.present? && phase == "Running" + + created_at = DateTime.parse(metadata["creationTimestamp"]) rescue nil + + containers.map do |container| + { + selectors: { pod: pod_name, container: container["name"] }, + url: container_exec_url(api_url, namespace, pod_name, container["name"]), + subprotocols: ['channel.k8s.io'], + headers: Hash.new { |h, k| h[k] = [] }, + created_at: created_at, + } + end + end + + def add_terminal_auth(terminal, token, ca_pem = nil) + terminal[:headers]['Authorization'] << "Bearer #{token}" + terminal[:ca_pem] = ca_pem if ca_pem.present? + terminal + end + + def container_exec_url(api_url, namespace, pod_name, container_name) + url = URI.parse(api_url) + url.path = [ + url.path.sub(%r{/+\z}, ''), + 'api', 'v1', + 'namespaces', ERB::Util.url_encode(namespace), + 'pods', ERB::Util.url_encode(pod_name), + 'exec' + ].join('/') + + url.query = { + container: container_name, + tty: true, + stdin: true, + stdout: true, + stderr: true, + }.to_query + '&' + EXEC_COMMAND + + case url.scheme + when 'http' + url.scheme = 'ws' + when 'https' + url.scheme = 'wss' + end + + url.to_s + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 7e06bd2b0fb5c7c3b4debcd0c1067392f995bb61..54a5b1d31cd4d80c8e898619715c1e45634066bb 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -34,21 +34,21 @@ module Gitlab def allowed? if ldap_user unless ldap_config.active_directory - user.activate if user.ldap_blocked? + unblock_user(user, 'is available again') if user.ldap_blocked? return true end # Block user in GitLab if he/she was blocked in AD if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) - user.ldap_block + block_user(user, 'is disabled in Active Directory') false else - user.activate if user.ldap_blocked? + unblock_user(user, 'is not disabled anymore') if user.ldap_blocked? true end else # Block the user if they no longer exist in LDAP/AD - user.ldap_block + block_user(user, 'does not exist anymore') false end end @@ -64,6 +64,24 @@ module Gitlab def ldap_user @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) end + + def block_user(user, reason) + user.ldap_block + + Gitlab::AppLogger.info( + "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + + def unblock_user(user, reason) + user.activate + + Gitlab::AppLogger.info( + "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ + "unblocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end end end end diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index bf4dd9542d5d4aea9ec687ddaf8182176b5b7c7a..95378e5a7693039bbc676de71fceb1dad0d6a4b0 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -25,7 +25,7 @@ module Gitlab end def get_raw(key) - auth_hash.extra[:raw_info][key] + auth_hash.extra[:raw_info][key] if auth_hash.extra end def ldap_config diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index de52ef3fc657fea039a202b4f3e8dca0700f8811..281291984381858b7bf119a5ab32bbc0723d60d7 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -107,7 +107,7 @@ module Gitlab end def attributes - options['attributes'] + default_attributes.merge(options['attributes']) end def timeout @@ -130,6 +130,16 @@ module Gitlab end end + def default_attributes + { + 'username' => %w(uid userid sAMAccountName), + 'email' => %w(mail email userPrincipalName), + 'name' => 'cn', + 'first_name' => 'givenName', + 'last_name' => 'sn' + } + end + protected def base_options diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index b81f3e8e8f5b8d18312cc583b5239de090663853..7084fd1767dceaee83abb012a440c212181e3624 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -28,7 +28,7 @@ module Gitlab end def name - entry.cn.first + attribute_value(:name).first end def uid @@ -40,7 +40,7 @@ module Gitlab end def email - entry.try(:mail) + attribute_value(:email) end def dn @@ -56,6 +56,19 @@ module Gitlab def config @config ||= Gitlab::LDAP::Config.new(provider) end + + # Using the LDAP attributes configuration, find and return the first + # attribute with a value. For example, by default, when given 'email', + # this method looks for 'mail', 'email' and 'userPrincipalName' and + # returns the first with a value. + def attribute_value(attribute) + attributes = Array(config.attributes[attribute.to_s]) + selected_attr = attributes.find { |attr| entry.respond_to?(attr) } + + return nil unless selected_attr + + entry.public_send(selected_attr) + end end end end diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index 01c96a6fe960f223a7fce9b836291a83d39fd328..47f88727fc8d6c3aae1db6d2ddfd6d3822f66172 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -70,8 +70,19 @@ module Gitlab def tag_endpoint(trans, env) endpoint = env[ENDPOINT_KEY] - path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path] - trans.action = "Grape##{endpoint.route.route_method} #{path}" + + begin + route = endpoint.route + rescue + # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info] + # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response + # so we're rescuing exceptions and bailing out + end + + if route + path = endpoint_paths_cache[route.request_method][route.path] + trans.action = "Grape##{route.request_method} #{path}" + end end private diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd99f9bb7d76293abe156f9e7a99a9103c976a22 --- /dev/null +++ b/lib/gitlab/middleware/multipart.rb @@ -0,0 +1,103 @@ +# Gitlab::Middleware::Multipart - a Rack::Multipart replacement +# +# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby +# process time to copy files around. This alternative solution uses +# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a +# location where copying should not be needed. +# +# When gitlab-workhorse finds files in a multipart MIME body it sends +# a signed message via a request header. This message lists the names of +# the multipart entries that gitlab-workhorse filtered out of the +# multipart structure and saved to tempfiles. Workhorse adds new entries +# in the multipart structure with paths to the tempfiles. +# +# The job of this Rack middleware is to detect and decode the message +# from workhorse. If present, it walks the Rack 'params' hash for the +# current request, opens the respective tempfiles, and inserts the open +# Ruby File objects in the params hash where Rack::Multipart would have +# put them. The goal is that application code deeper down can keep +# working the way it did with Rack::Multipart without changes. +# +# CAVEAT: the code that modifies the params hash is a bit complex. It is +# conceivable that certain Rack params structures will not be modified +# correctly. We are not aware of such bugs at this time though. +# + +module Gitlab + module Middleware + class Multipart + RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS' + + class Handler + def initialize(env, message) + @request = Rack::Request.new(env) + @rewritten_fields = message['rewritten_fields'] + @open_files = [] + end + + def with_open_files + @rewritten_fields.each do |field, tmp_path| + parsed_field = Rack::Utils.parse_nested_query(field) + raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 + + key, value = parsed_field.first + if value.nil? + value = open_file(tmp_path) + @open_files << value + else + value = decorate_params_value(value, @request.params[key], tmp_path) + end + @request.update_param(key, value) + end + + yield + ensure + @open_files.each(&:close) + end + + # This function calls itself recursively + def decorate_params_value(path_hash, value_hash, tmp_path) + unless path_hash.is_a?(Hash) && path_hash.count == 1 + raise "invalid path: #{path_hash.inspect}" + end + path_key, path_value = path_hash.first + + unless value_hash.is_a?(Hash) && value_hash[path_key] + raise "invalid value hash: #{value_hash.inspect}" + end + + case path_value + when nil + value_hash[path_key] = open_file(tmp_path) + @open_files << value_hash[path_key] + value_hash + when Hash + decorate_params_value(path_value, value_hash[path_key], tmp_path) + value_hash + else + raise "unexpected path value: #{path_value.inspect}" + end + end + + def open_file(path) + ::UploadedFile.new(path, File.basename(path), 'application/octet-stream') + end + end + + def initialize(app) + @app = app + end + + def call(env) + encoded_message = env.delete(RACK_ENV_KEY) + return @app.call(env) if encoded_message.blank? + + message = Gitlab::Workhorse.decode_jwt(encoded_message)[0] + + Handler.new(env, message).with_open_files do + @app.call(env) + end + end + end + end +end diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index cc74bb29087c7fd56cc0d7d07b3f57131fd5af53..4bc5cda8cb587ee32ab9f3fd1e88a66ef5925367 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -5,13 +5,13 @@ module Gitlab module Popen extend self - def popen(cmd, path = nil) + def popen(cmd, path = nil, vars = {}) unless cmd.is_a?(Array) raise "System commands must be given as an array of strings" end path ||= Dir.pwd - vars = { "PWD" => path } + vars['PWD'] = path options = { chdir: path } unless File.directory?(path) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 66e6b29e798b2d37a2edbff6200b468195064eb4..6bdf3db9cb88892b01053fd001bf750e8a4b6fab 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -110,7 +110,7 @@ module Gitlab end def notes - @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') + @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC') end def commits diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 9226da2d6b1e03c6e0351a4c69213a7336d15e93..9384102acec65adc0ce3bd3bfe408363f0422d28 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -42,7 +42,7 @@ module Gitlab return @_raw_config if defined?(@_raw_config) begin - @_raw_config = File.read(CONFIG_FILE).freeze + @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze rescue Errno::ENOENT @_raw_config = false end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index d9d1e3cccca858ea5960e998b2f44d7253e020de..a3fa7c1331a3a022c366047f6267f8581937bbdc 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -61,11 +61,11 @@ module Gitlab end def file_name_regex - @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze + @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze end def file_name_regex_message - "can contain only letters, digits, '_', '-', '@' and '.'." + "can contain only letters, digits, '_', '-', '@', '+' and '.'." end def file_path_regex @@ -123,5 +123,22 @@ module Gitlab def environment_name_regex_message "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces" end + + def kubernetes_namespace_regex + /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/ + end + + def kubernetes_namespace_regex_message + "can contain only letters, digits or '-', and cannot start or end with '-'" + end + + def environment_slug_regex + @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze + end + + def environment_slug_regex_message + "can contain only lowercase letters, digits, and '-'. " \ + "Must start with a letter, and cannot end with '-'" + end end end diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb index 5132177de516ee6b9eab38a57adfd87e161169b3..632e2d875000c9fccb8284d49edadd4d8bdbbb2f 100644 --- a/lib/gitlab/routing.rb +++ b/lib/gitlab/routing.rb @@ -1,5 +1,11 @@ module Gitlab module Routing + extend ActiveSupport::Concern + + included do + include Gitlab::Routing.url_helpers + end + # Returns the URL helpers Module. # # This method caches the output as Rails' "url_helpers" method creates an diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serialize/ci/variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..3a9443bfcd981e7fae080836c79a69f20b6a5f3e --- /dev/null +++ b/lib/gitlab/serialize/ci/variables.rb @@ -0,0 +1,27 @@ +module Gitlab + module Serialize + module Ci + # This serializer could make sure our YAML variables' keys and values + # are always strings. This is more for legacy build data because + # from now on we convert them into strings before saving to database. + module Variables + extend self + + def load(string) + return unless string + + object = YAML.safe_load(string, [Symbol]) + + object.map do |variable| + variable[:key] = variable[:key].to_s + variable + end + end + + def dump(object) + YAML.dump(object) + end + end + end + end +end diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 1cd89b3a9c40b5de369023805e0942c014be5e3d..222021e8802e5e970a6074eb0185f85f6c45e905 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -22,9 +22,7 @@ module Gitlab # By using "unprepared_statements" we remove the usage of placeholders # (thus fixing this problem), at a slight performance cost. fragments = ActiveRecord::Base.connection.unprepared_statement do - @relations.map do |rel| - rel.reorder(nil).to_sql - end + @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end fragments.join("\nUNION\n") diff --git a/lib/gitlab/template/dockerfile_template.rb b/lib/gitlab/template/dockerfile_template.rb new file mode 100644 index 0000000000000000000000000000000000000000..d5d3e045a42d8db432f7436cdbdd30c2a4a581c6 --- /dev/null +++ b/lib/gitlab/template/dockerfile_template.rb @@ -0,0 +1,30 @@ +module Gitlab + module Template + class DockerfileTemplate < BaseTemplate + def content + explanation = "# This file is a template, and might need editing before it works on your project." + [explanation, super].join("\n") + end + + class << self + def extension + 'Dockerfile' + end + + def categories + { + "General" => '' + } + end + + def base_dir + Rails.root.join('vendor/dockerfile') + end + + def finder(project = nil) + Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories) + end + end + end + end +end diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb index 8d1a1ed54c9db399cc4c64f7a608baf9bf1ab36a..9d2ecee97569583d91eaa9c5659c789b45e5e097 100644 --- a/lib/gitlab/template/gitlab_ci_yml_template.rb +++ b/lib/gitlab/template/gitlab_ci_yml_template.rb @@ -13,8 +13,9 @@ module Gitlab def categories { - "General" => '', - "Pages" => 'Pages' + 'General' => '', + 'Pages' => 'Pages', + 'Auto deploy' => 'autodeploy' } end @@ -25,6 +26,11 @@ module Gitlab def finder(project = nil) Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories) end + + def dropdown_names(context) + categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages'] + super().slice(*categories) + end end end end diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index d4020af76f9af2f87f4c324495091eee5d41a345..19ab76ae80f6a6b26d3e5c694e76511b14700499 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -15,7 +15,7 @@ module Gitlab Theme.new(1, 'Graphite', 'ui_graphite'), Theme.new(2, 'Charcoal', 'ui_charcoal'), Theme.new(3, 'Green', 'ui_green'), - Theme.new(4, 'Gray', 'ui_gray'), + Theme.new(4, 'Black', 'ui_black'), Theme.new(5, 'Violet', 'ui_violet'), Theme.new(6, 'Blue', 'ui_blue') ].freeze diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb new file mode 100644 index 0000000000000000000000000000000000000000..d615c24149aeff750cc664b153efbba807e892d7 --- /dev/null +++ b/lib/gitlab/time_tracking_formatter.rb @@ -0,0 +1,34 @@ +module Gitlab + module TimeTrackingFormatter + extend self + + def parse(string) + with_custom_config do + string.sub!(/\A-/, '') + + seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil + seconds *= -1 if seconds && Regexp.last_match + seconds + end + end + + def output(seconds) + with_custom_config do + ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil + end + end + + def with_custom_config + # We may want to configure it through project settings in a future version. + ChronicDuration.hours_per_day = 8 + ChronicDuration.days_per_week = 5 + + result = yield + + ChronicDuration.hours_per_day = 24 + ChronicDuration.days_per_week = 7 + + result + end + end +end diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb new file mode 100644 index 0000000000000000000000000000000000000000..ce14cc887d0f4edfd674c9df14b2feeb2274d1e4 --- /dev/null +++ b/lib/gitlab/update_path_error.rb @@ -0,0 +1,3 @@ +module Gitlab + class UpdatePathError < StandardError; end +end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 9858d2e7d83b64404a5a8de49a7b74fab0a54821..6c7e673fb9fbf332714a07d18a4c26fba75c0edb 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -8,6 +8,8 @@ module Gitlab end def can_do_action?(action) + return false if no_user_or_blocked? + @permission_cache ||= {} @permission_cache[action] ||= user.can?(action, project) end @@ -17,7 +19,7 @@ module Gitlab end def allowed? - return false if user.blank? || user.blocked? + return false if no_user_or_blocked? if user.requires_ldap_check? && user.try_obtain_ldap_lease return false unless Gitlab::LDAP::Access.allowed?(user) @@ -27,7 +29,7 @@ module Gitlab end def can_push_to_branch?(ref) - return false unless user + return false if no_user_or_blocked? if project.protected_branch?(ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) @@ -40,7 +42,7 @@ module Gitlab end def can_merge_to_branch?(ref) - return false unless user + return false if no_user_or_blocked? if project.protected_branch?(ref) access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten @@ -51,9 +53,15 @@ module Gitlab end def can_read_project? - return false unless user + return false if no_user_or_blocked? user.can?(:read_project, project) end + + private + + def no_user_or_blocked? + user.nil? || user.blocked? + end end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 594439a5d4b3a6f979d362e58ebf39187faf651e..d28bb583fe772d26f20a7e206aee0bcd1368aa04 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -95,6 +95,19 @@ module Gitlab ] end + def terminal_websocket(terminal) + details = { + 'Terminal' => { + 'Subprotocols' => terminal[:subprotocols], + 'Url' => terminal[:url], + 'Header' => terminal[:headers] + } + } + details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem) + + details + end + def version path = Rails.root.join(VERSION_FILE) path.readable? ? path.read.chomp : 'unknown' @@ -117,8 +130,12 @@ module Gitlab end def verify_api_request!(request_headers) + decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER]) + end + + def decode_jwt(encoded_message) JWT.decode( - request_headers[INTERNAL_API_REQUEST_HEADER], + encoded_message, secret, true, { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' }, diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec2903b7ec6c1daf7bdeea3320155da2c8149c81 --- /dev/null +++ b/lib/mattermost/client.rb @@ -0,0 +1,41 @@ +module Mattermost + class ClientError < Mattermost::Error; end + + class Client + attr_reader :user + + def initialize(user) + @user = user + end + + private + + def with_session(&blk) + Mattermost::Session.new(user).with_session(&blk) + end + + def json_get(path, options = {}) + with_session do |session| + json_response session.get(path, options) + end + end + + def json_post(path, options = {}) + with_session do |session| + json_response session.post(path, options) + end + end + + def json_response(response) + json_response = JSON.parse(response.body) + + unless response.success? + raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error') + end + + json_response + rescue JSON::JSONError + raise Mattermost::ClientError.new('Cannot parse response') + end + end +end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb new file mode 100644 index 0000000000000000000000000000000000000000..d1e4bb0eccf83cc0ff4d1af3f4be6e1edbd19ee3 --- /dev/null +++ b/lib/mattermost/command.rb @@ -0,0 +1,10 @@ +module Mattermost + class Command < Client + def create(params) + response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", + body: params.to_json) + + response['token'] + end + end +end diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb new file mode 100644 index 0000000000000000000000000000000000000000..014df175be09d87b0a3fd218363beaf6ca41cd5c --- /dev/null +++ b/lib/mattermost/error.rb @@ -0,0 +1,3 @@ +module Mattermost + class Error < StandardError; end +end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 0000000000000000000000000000000000000000..377cb7b10211f4b8101d923d6fb99c0f0500cb32 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,160 @@ +module Mattermost + class NoSessionError < Mattermost::Error + def message + 'No session could be set up, is Mattermost configured with Single Sign On?' + end + end + + class ConnectionError < Mattermost::Error; end + + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Session + include Doorkeeper::Helpers::Controller + include HTTParty + + LEASE_TIMEOUT = 60 + + base_uri Settings.mattermost.host + + attr_accessor :current_resource_owner, :token + + def initialize(current_user) + @current_resource_owner = current_user + end + + def with_session + with_lease do + raise Mattermost::NoSessionError unless create + + begin + yield self + rescue Errno::ECONNREFUSED + raise Mattermost::NoSessionError + ensure + destroy + end + end + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(oauth_uri.query).symbolize_keys + end + + def get(path, options = {}) + handle_exceptions do + self.class.get(path, options.merge(headers: @headers)) + end + end + + def post(path, options = {}) + handle_exceptions do + self.class.post(path, options.merge(headers: @headers)) + end + end + + private + + def create + return unless oauth_uri + return unless token_uri + + @token = request_token + @headers = { + Authorization: "Bearer #{@token}" + } + + @token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + return @oauth_uri if defined?(@oauth_uri) + + @oauth_uri = nil + + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri = URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= + if oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + response = get(token_uri, follow_redirects: false) + + if 200 <= response.code && response.code < 400 + response.headers['token'] + end + end + + def with_lease + lease_uuid = lease_try_obtain + raise NoSessionError unless lease_uuid + + begin + yield + ensure + Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) + end + end + + def lease_key + "mattermost:session" + end + + def lease_try_obtain + lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + lease.try_obtain + end + + def handle_exceptions + yield + rescue HTTParty::Error => e + raise Mattermost::ConnectionError.new(e.message) + rescue Errno::ECONNREFUSED + raise Mattermost::ConnectionError.new(e.message) + end + end +end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb new file mode 100644 index 0000000000000000000000000000000000000000..784eca6ab5a61e5f048515ac296873f970039ba7 --- /dev/null +++ b/lib/mattermost/team.rb @@ -0,0 +1,7 @@ +module Mattermost + class Team < Client + def all + json_get('/api/v3/teams/all') + end + end +end diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb new file mode 100644 index 0000000000000000000000000000000000000000..5a7d67c23903e3efb6163f7abd859e8e414d1678 --- /dev/null +++ b/lib/omniauth/strategies/bitbucket.rb @@ -0,0 +1,41 @@ +require 'omniauth-oauth2' + +module OmniAuth + module Strategies + class Bitbucket < OmniAuth::Strategies::OAuth2 + option :name, 'bitbucket' + + option :client_options, { + site: 'https://bitbucket.org', + authorize_url: 'https://bitbucket.org/site/oauth2/authorize', + token_url: 'https://bitbucket.org/site/oauth2/access_token' + } + + uid do + raw_info['username'] + end + + info do + { + name: raw_info['display_name'], + avatar: raw_info['links']['avatar']['href'], + email: primary_email + } + end + + def raw_info + @raw_info ||= access_token.get('api/2.0/user').parsed + end + + def primary_email + primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] } + primary && primary['email'] || nil + end + + def emails + email_response = access_token.get('api/2.0/user/emails').parsed + @emails ||= email_response && email_response['values'] || nil + end + end + end +end diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb new file mode 100644 index 0000000000000000000000000000000000000000..80784adfd76930d603e3d7f74e60e5cde2d77a8a --- /dev/null +++ b/lib/rouge/lexers/math.rb @@ -0,0 +1,21 @@ +module Rouge + module Lexers + class Math < Lexer + title "A passthrough lexer used for LaTeX input" + desc "A boring lexer that doesn't highlight anything" + + tag 'math' + mimetypes 'text/plain' + + default_options token: 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index d521de28e8a293a943f5bfa57f54e1cf6ed2f9f0..2f7c34a3f31f1e04b2b4be1a5c4a907b901b375a 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -20,6 +20,11 @@ upstream gitlab-workhorse { server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } +map $http_upgrade $connection_upgrade_gitlab { + default upgrade; + '' close; +} + ## Normal HTTP host server { ## Either remove "default_server" from the listen line below, @@ -53,6 +58,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade_gitlab; proxy_pass http://gitlab-workhorse; } diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index bf014b56cf68062d3da68c1416aea7e7d09c83de..5661394058db939834dc44df3ede3035fb27dac6 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -24,6 +24,11 @@ upstream gitlab-workhorse { server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } +map $http_upgrade $connection_upgrade_gitlab_ssl { + default upgrade; + '' close; +} + ## Redirects all HTTP traffic to the HTTPS host server { ## Either remove "default_server" from the listen line below, @@ -98,6 +103,9 @@ server { proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade_gitlab_ssl; + proxy_pass http://gitlab-workhorse; } diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 6f27972c4e4667c9d1c7db33c73ec5777acfd6fb..5e94fba97bf1e8273e463575262d1844e4b5e73d 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -7,9 +7,4 @@ namespace :dev do Rake::Task["gitlab:setup"].invoke Rake::Task["gitlab:shell:setup"].invoke end - - desc 'GitLab | Start/restart foreman and watch for changes' - task :foreman => :environment do - sh 'rerun --dir app,config,lib -- foreman start' - end end diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index f9834a4dae8441f21f92ade58cdf4d32085e8b28..a67c1fe1f279ffb3c519c89c5693e057f02c365d 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -3,7 +3,7 @@ namespace :gitlab do desc "GitLab | Git | Repack" task repack: :environment do - failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo") if failures.empty? puts "Done".color(:green) else @@ -13,17 +13,17 @@ namespace :gitlab do desc "GitLab | Git | Run garbage collection on all repos" task gc: :environment do - failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting") if failures.empty? puts "Done".color(:green) else output_failures(failures) end end - + desc "GitLab | Git | Prune all repos" task prune: :environment do - failures = perform_git_cmd(%W(git prune), "Git Prune") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune") if failures.empty? puts "Done".color(:green) else diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index dbdd4e977e8becc230551f32abbda26a691ab8b6..a2eca74a3c833bddde99d6b9a2e704eb65759182 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -63,8 +63,7 @@ namespace :gitlab do if project.persisted? puts " * Created #{project.name} (#{repo_path})".color(:green) - project.update_repository_size - project.update_commit_count + ProjectCacheWorker.perform(project.id) else puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) puts " Errors: #{project.errors.messages}".color(:red) diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake new file mode 100644 index 0000000000000000000000000000000000000000..c66a2a263dcb143bc4e711727d5746d634589fe0 --- /dev/null +++ b/lib/tasks/gitlab/ldap.rake @@ -0,0 +1,40 @@ +namespace :gitlab do + namespace :ldap do + desc 'GitLab | LDAP | Rename provider' + task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args| + old_provider = args[:old_provider] || + prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue)) + new_provider = args[:new_provider] || + prompt('What is the new provider ID? Ex. \'ldapcustom\': '.color(:blue)) + puts '' # Add some separation in the output + + identities = Identity.where(provider: old_provider) + identity_count = identities.count + + if identities.empty? + puts "Found no user identities with '#{old_provider}' provider." + puts 'Please check the provider name and try again.' + exit 1 + end + + plural_id_count = ActionController::Base.helpers.pluralize(identity_count, 'user') + + unless ENV['force'] == 'yes' + puts "#{plural_id_count} with provider '#{old_provider}' will be updated to '#{new_provider}'" + puts 'If the new provider is incorrect, users will be unable to sign in' + ask_to_continue + puts '' + end + + updated_count = identities.update_all(provider: new_provider) + + if updated_count == identity_count + puts 'User identities were successfully updated'.color(:green) + else + plural_updated_count = ActionController::Base.helpers.pluralize(updated_count, 'user') + puts 'Some user identities could not be updated'.color(:red) + puts "Successfully updated #{plural_updated_count} out of #{plural_id_count} total" + end + end + end +end diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake deleted file mode 100644 index 3bd10b0208bf7fa78cc40c784d871715c8767047..0000000000000000000000000000000000000000 --- a/lib/tasks/gitlab/update_commit_count.rake +++ /dev/null @@ -1,20 +0,0 @@ -namespace :gitlab do - desc "GitLab | Update commit count for projects" - task update_commit_count: :environment do - projects = Project.where(commit_count: 0) - puts "#{projects.size} projects need to be updated. This might take a while." - ask_to_continue unless ENV['force'] == 'yes' - - projects.find_each(batch_size: 100) do |project| - print "#{project.name_with_namespace.color(:yellow)} ... " - - unless project.repo_exists? - puts "skipping, because the repo is empty".color(:magenta) - next - end - - project.update_commit_count - puts project.commit_count.to_s.color(:green) - end - end -end diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index 4f76dad728633ea8a91d4a4f8abd9b71fac4b431..b77a5bb62d1435619eb259472dc3f30d31b3c8d7 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -44,7 +44,7 @@ namespace :gitlab do ), Template.new( "https://gitlab.com/gitlab-org/gitlab-ci-yml.git", - /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/ + /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/ ) ] diff --git a/public/404.html b/public/404.html index 11b29d09a82981401004e1d4da06b65bfc28cbb7..b3b3a0fa3f30f7d03cebe562a3925da3a2b438fc 100644 --- a/public/404.html +++ b/public/404.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure the address is correct and that the page hasn't moved.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/422.html b/public/422.html index 9bd7cb4b7c82f7c4aac87ac639a1670cd30d4982..119e54ad8bd4fc2cc90390748e4d65c95e54244b 100644 --- a/public/422.html +++ b/public/422.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure you have access to the thing you tried to change.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/500.html b/public/500.html index f92e8839f8d9601e53373a810a5f4281c1ab3598..226ef3c40eaebe2b90b779635a54e50f084d6b70 100644 --- a/public/500.html +++ b/public/500.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/502.html b/public/502.html index c2be4f130a95d603aa480053a378385e7ec5fc5a..f037b81bace0a3d160721ed30b57e7a0ac3adfd2 100644 --- a/public/502.html +++ b/public/502.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/503.html b/public/503.html index 8850ffce362bc91f7053aee28d3a0c358a907848..f946a087871782c3fdce154ce0e74d2a71082fdc 100644 --- a/public/503.html +++ b/public/503.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/slash-command-logo.png b/public/slash-command-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..05c8b0d0ccfdcefe43cdb1dae5e6fa0fa8366dae Binary files /dev/null and b/public/slash-command-logo.png differ diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh index 0a4239e132c77f9058e1a4f5f631aa612f5f0238..6b3bc563c7a34070b5fbf1d346428369b8db421a 100755 --- a/scripts/notify_slack.sh +++ b/scripts/notify_slack.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Sends Slack notification ERROR_MSG to CHANNEL # An env. variable CI_SLACK_WEBHOOK_URL needs to be set. diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..19fbc2f7748d297a80f266166ebdde89e08c9e37 --- /dev/null +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Dashboard::TodosController do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:todo_service) { TodoService.new } + + describe 'GET #index' do + before do + sign_in(user) + project.team << [user, :developer] + end + + context 'when using pagination' do + let(:last_page) { user.todos.page.total_pages } + let!(:issues) { create_list(:issue, 2, project: project, assignee: user) } + + before do + issues.each { |issue| todo_service.new_issue(issue, user) } + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + end + + it 'redirects to last_page if page number is larger than number of pages' do + get :index, page: (last_page + 1).to_param + + expect(response).to redirect_to(dashboard_todos_path(page: last_page)) + end + + it 'redirects to correspondent page' do + get :index, page: last_page + + expect(assigns(:todos).current_page).to eq(last_page) + expect(response).to have_http_status(200) + end + end + end +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a763e2c5ba852a31fc3c114fbdf81aaadb46bc86..98dfb3e52165fe35777f509171c4f7194de66989 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -105,4 +105,25 @@ describe GroupsController do end end end + + describe 'PUT update' do + before do + sign_in(user) + end + + it 'updates the path succesfully' do + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(response).to have_http_status(302) + expect(controller).to set_flash[:notice] + end + + it 'does not update the path on error' do + allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(assigns(:group).errors).not_to be_empty + expect(assigns(:group).path).not_to eq('new_path') + end + end end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 1d3c9fbbe2f49d5a0d109ae616f2838478c5317e..ce7c0b334ee071358b935322165ed9874c6bdc2d 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -6,11 +6,11 @@ describe Import::BitbucketController do let(:user) { create(:user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } - let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } + let(:refresh_token) { SecureRandom.hex(15) } + let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } } def assign_session_tokens - session[:bitbucket_access_token] = token - session[:bitbucket_access_token_secret] = secret + session[:bitbucket_token] = token end before do @@ -24,29 +24,36 @@ describe Import::BitbucketController do end it "updates access token" do - access_token = double(token: token, secret: secret) - allow_any_instance_of(Gitlab::BitbucketImport::Client). + expires_at = Time.now + 1.day + expires_in = 1.day + access_token = double(token: token, + secret: secret, + expires_at: expires_at, + expires_in: expires_in, + refresh_token: refresh_token) + allow_any_instance_of(OAuth2::Client). to receive(:get_token).and_return(access_token) stub_omniauth_provider('bitbucket') get :callback - expect(session[:bitbucket_access_token]).to eq(token) - expect(session[:bitbucket_access_token_secret]).to eq(secret) + expect(session[:bitbucket_token]).to eq(token) + expect(session[:bitbucket_refresh_token]).to eq(refresh_token) + expect(session[:bitbucket_expires_at]).to eq(expires_at) + expect(session[:bitbucket_expires_in]).to eq(expires_in) expect(controller).to redirect_to(status_import_bitbucket_url) end end describe "GET status" do before do - @repo = OpenStruct.new(slug: 'vim', owner: 'asd') + @repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true) assign_session_tokens end it "assigns variables" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id) - client = stub_client(projects: [@repo]) - allow(client).to receive(:incompatible_projects).and_return([]) + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) get :status @@ -57,7 +64,7 @@ describe Import::BitbucketController do it "does not show already added project" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim') - stub_client(projects: [@repo]) + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) get :status @@ -70,19 +77,16 @@ describe Import::BitbucketController do let(:bitbucket_username) { user.username } let(:bitbucket_user) do - { user: { username: bitbucket_username } }.with_indifferent_access + double(username: bitbucket_username) end let(:bitbucket_repo) do - { slug: "vim", owner: bitbucket_username }.with_indifferent_access + double(slug: "vim", owner: bitbucket_username, name: 'vim') end before do - allow(Gitlab::BitbucketImport::KeyAdder). - to receive(:new).with(bitbucket_repo, user, access_params). - and_return(double(execute: true)) - - stub_client(user: bitbucket_user, project: bitbucket_repo) + allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo) + allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user) assign_session_tokens end @@ -90,7 +94,7 @@ describe Import::BitbucketController do context "when the Bitbucket user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -102,7 +106,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -114,7 +118,7 @@ describe Import::BitbucketController do let(:other_username) { "someone_else" } before do - bitbucket_repo["owner"] = other_username + allow(bitbucket_repo).to receive(:owner).and_return(other_username) end context "when a namespace with the Bitbucket user's username already exists" do @@ -123,7 +127,7 @@ describe Import::BitbucketController do context "when the namespace is owned by the GitLab user" do it "takes the existing namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -156,7 +160,7 @@ describe Import::BitbucketController do it "takes the new namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -177,7 +181,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ba64ab3eed71119b067cf683c5339dabab5098f --- /dev/null +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Import::GiteaController do + include ImportSpecHelper + + let(:provider) { :gitea } + let(:host_url) { 'https://try.gitea.io' } + + include_context 'a GitHub-ish import controller' + + def assign_host_url + session[:gitea_host_url] = host_url + end + + describe "GET new" do + it_behaves_like 'a GitHub-ish import controller: GET new' do + before do + assign_host_url + end + end + end + + describe "POST personal_access_token" do + it_behaves_like 'a GitHub-ish import controller: POST personal_access_token' + end + + describe "GET status" do + it_behaves_like 'a GitHub-ish import controller: GET status' do + before do + assign_host_url + end + let(:extra_assign_expectations) { { gitea_host_url: host_url } } + end + end + + describe 'POST create' do + it_behaves_like 'a GitHub-ish import controller: POST create' do + before do + assign_host_url + end + end + end +end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 4f96567192dccf2a60a2e8ef5465111b189a79ca..95696e14b6c5db12cb77e77fe84e05edcfa16c93 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -3,34 +3,18 @@ require 'spec_helper' describe Import::GithubController do include ImportSpecHelper - let(:user) { create(:user) } - let(:token) { "asdasd12345" } - let(:access_params) { { github_access_token: token } } + let(:provider) { :github } - def assign_session_token - session[:github_access_token] = token - end - - before do - sign_in(user) - allow(controller).to receive(:github_import_enabled?).and_return(true) - end + include_context 'a GitHub-ish import controller' describe "GET new" do - it "redirects to GitHub for an access token if logged in with GitHub" do - allow(controller).to receive(:logged_in_with_github?).and_return(true) - expect(controller).to receive(:go_to_github_for_permissions) + it_behaves_like 'a GitHub-ish import controller: GET new' - get :new - end - - it "redirects to status if we already have a token" do - assign_session_token - allow(controller).to receive(:logged_in_with_github?).and_return(false) + it "redirects to GitHub for an access token if logged in with GitHub" do + allow(controller).to receive(:logged_in_with_provider?).and_return(true) + expect(controller).to receive(:go_to_provider_for_permissions) get :new - - expect(controller).to redirect_to(status_import_github_url) end end @@ -51,196 +35,14 @@ describe Import::GithubController do end describe "POST personal_access_token" do - it "updates access token" do - token = "asdfasdf9876" - - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:user).and_return(true) - - post :personal_access_token, personal_access_token: token - - expect(session[:github_access_token]).to eq(token) - expect(controller).to redirect_to(status_import_github_url) - end + it_behaves_like 'a GitHub-ish import controller: POST personal_access_token' end describe "GET status" do - before do - @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim') - @org = OpenStruct.new(login: 'company') - @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo') - assign_session_token - end - - it "assigns variables" do - @project = create(:project, import_type: 'github', creator_id: user.id) - stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo, @org_repo]) - end - - it "does not show already added project" do - @project = create(:project, import_type: 'github', creator_id: user.id, import_source: 'asd/vim') - stub_client(repos: [@repo], orgs: []) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end - - it "handles an invalid access token" do - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:repos).and_raise(Octokit::Unauthorized) - - get :status - - expect(session[:github_access_token]).to eq(nil) - expect(controller).to redirect_to(new_import_github_url) - expect(flash[:alert]).to eq('Access denied to your GitHub account.') - end + it_behaves_like 'a GitHub-ish import controller: GET status' end describe "POST create" do - let(:github_username) { user.username } - let(:github_user) { OpenStruct.new(login: github_username) } - let(:github_repo) do - OpenStruct.new( - name: 'vim', - full_name: "#{github_username}/vim", - owner: OpenStruct.new(login: github_username) - ) - end - - before do - stub_client(user: github_user, repo: github_repo) - assign_session_token - end - - context "when the repository owner is the GitHub user" do - context "when the GitHub user and GitLab user's usernames match" do - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the GitHub user and GitLab user's usernames don't match" do - let(:github_username) { "someone_else" } - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when the repository owner is not the GitHub user" do - let(:other_username) { "someone_else" } - - before do - github_repo.owner = OpenStruct.new(login: other_username) - assign_session_token - end - - context "when a namespace with the GitHub user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } - - context "when the namespace is owned by the GitLab user" do - it "takes the existing namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the namespace is not owned by the GitLab user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - - it "creates a project using user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when a namespace with the GitHub user's username doesn't exist" do - context "when current user can create namespaces" do - it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1) - end - - it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) - - post :create, target_namespace: github_repo.name, format: :js - end - end - - context "when current user can't create namespaces" do - before do - user.update_attribute(:can_create_group, false) - end - - it "doesn't create the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, format: :js }.not_to change(Namespace, :count) - end - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context 'user has chosen a namespace and name for the project' do - let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } - let(:test_name) { 'test_name' } - - it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, test_name, test_namespace, user, access_params). - and_return(double(execute: true)) - - post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } - end - - it 'takes the selected name and default namespace' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, test_name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, { new_name: test_name, format: :js } - end - end - end + it_behaves_like 'a GitHub-ish import controller: POST create' end end diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..45534a3a5875abf205c55a853f753bf66024a5b8 --- /dev/null +++ b/spec/controllers/profiles/personal_access_tokens_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Profiles::PersonalAccessTokensController do + let(:user) { create(:user) } + + describe '#create' do + def created_token + PersonalAccessToken.order(:created_at).last + end + + before { sign_in(user) } + + it "allows creation of a token" do + name = FFaker::Product.brand + + post :create, personal_access_token: { name: name } + + expect(created_token).not_to be_nil + expect(created_token.name).to eq(name) + expect(created_token.expires_at).to be_nil + expect(PersonalAccessToken.active).to include(created_token) + end + + it "allows creation of a token with an expiry date" do + expires_at = 5.days.from_now + + post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at } + + expect(created_token).not_to be_nil + expect(created_token.expires_at.to_i).to eq(expires_at.to_i) + end + + context "scopes" do + it "allows creation of a token with scopes" do + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] } + + expect(created_token).not_to be_nil + expect(created_token.scopes).to eq(['api', 'read_user']) + end + + it "allows creation of a token with no scopes" do + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] } + + expect(created_token).not_to be_nil + expect(created_token.scopes).to eq([]) + end + end + end +end diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb index 25f06299a29e0eb50ab3da1765d0f193d51d965d..4402ca43c65ccbb4bfed5d129494f2b76498eec9 100644 --- a/spec/controllers/projects/blame_controller_spec.rb +++ b/spec/controllers/projects/blame_controller_spec.rb @@ -25,5 +25,10 @@ describe Projects::BlameController do let(:id) { 'master/files/ruby/popen.rb' } it { is_expected.to respond_with(:success) } end + + context "invalid file" do + let(:id) { 'master/files/ruby/missing_file.rb'} + it { expect(response).to have_http_status(404) } + end end end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 3efef757ae2f6a241d17ef15ac4672bd20832cac..f35c5d992d95a64b9c28a5feca1528de36892c81 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe Projects::BlobController do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } before do @@ -84,5 +84,35 @@ describe Projects::BlobController do end end end + + context 'when user has forked project' do + let(:guest) { create(:user) } + let!(:forked_project) { Projects::ForkService.new(project, guest).execute } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") } + + before { sign_in(guest) } + + it "redirects to forked project new merge request" do + default_params[:target_branch] = "fork-test-1" + default_params[:create_merge_request] = 1 + + allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success) + + put :update, default_params + + expect(response).to redirect_to( + new_namespace_project_merge_request_path( + forked_project.namespace, + forked_project, + merge_request: { + source_project_id: forked_project.id, + target_project_id: project.id, + source_branch: "fork-test-1", + target_branch: "master" + } + ) + ) + end + end end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 7a57801c437c0e02ac4db39d356ddd8131ea33ab..b03c4b52de67c451a9c6266ab0967c714570732f 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -64,6 +64,36 @@ describe Projects::CompareController do expect(assigns(:diffs)).to eq(nil) expect(assigns(:commits)).to eq(nil) end + + it 'redirects back to index when params[:from] is empty and preserves params[:to]' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '', + to: 'master') + + expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, to: 'master')) + end + + it 'redirects back to index when params[:to] is empty and preserves params[:from]' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: 'master', + to: '') + + expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, from: 'master')) + end + + it 'redirects back to index when params[:from] and params[:to] are empty' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '', + to: '') + + expect(response).to redirect_to(namespace_project_compare_index_path) + end end describe 'GET diff_for_path' do diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index bc5e2711125afc33369c5de0dc10d3fcbe37c881..7ac1d62d1b1a63ddee9dae16289ddba4d062c752 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -71,6 +71,75 @@ describe Projects::EnvironmentsController do end end + describe 'GET #terminal' do + context 'with valid id' do + it 'responds with a status code 200' do + get :terminal, environment_params + + expect(response).to have_http_status(200) + end + + it 'loads the terminals for the enviroment' do + expect_any_instance_of(Environment).to receive(:terminals) + + get :terminal, environment_params + end + end + + context 'with invalid id' do + it 'responds with a status code 404' do + get :terminal, environment_params(id: 666) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET #terminal_websocket_authorize' do + context 'with valid workhorse signature' do + before do + allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil) + end + + context 'and valid id' do + it 'returns the first terminal for the environment' do + expect_any_instance_of(Environment). + to receive(:terminals). + and_return([:fake_terminal]) + + expect(Gitlab::Workhorse). + to receive(:terminal_websocket). + with(:fake_terminal). + and_return(workhorse: :response) + + get :terminal_websocket_authorize, environment_params + + expect(response).to have_http_status(200) + expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(response.body).to eq('{"workhorse":"response"}') + end + end + + context 'and invalid id' do + it 'returns 404' do + get :terminal_websocket_authorize, environment_params(id: 666) + + expect(response).to have_http_status(404) + end + end + end + + context 'with invalid workhorse signature' do + it 'aborts with an exception' do + allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError) + + expect { get :terminal_websocket_authorize, environment_params }.to raise_error(JWT::DecodeError) + # controller tests don't set the response status correctly. It's enough + # to check that the action raised an exception + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index b9d9117c928c4a14a51e59c4babba304d03cefed..17dc101b7ee63a4e9be6db33d3b77b999d2d8d64 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -31,7 +31,7 @@ describe Projects::GroupLinksController do it 'redirects to project group links page' do expect(response).to redirect_to( - namespace_project_group_links_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) end end @@ -62,7 +62,7 @@ describe Projects::GroupLinksController do it 'redirects to project group links page' do expect(response).to redirect_to( - namespace_project_group_links_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) end end @@ -76,7 +76,7 @@ describe Projects::GroupLinksController do it 'redirects to project group links page' do expect(response).to redirect_to( - namespace_project_group_links_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) expect(flash[:alert]).to eq('Please select a group.') end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index dbe5ddccbcfaa0b747fc5fba89005e29e8e35534..b5987a83df0affcea451c4d78e8b718d97eda9af 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -52,6 +52,36 @@ describe Projects::IssuesController do expect(response).to have_http_status(404) end end + + context 'with page param' do + let(:last_page) { project.issues.page().total_pages } + let!(:issue_list) { create_list(:issue, 2, project: project) } + + before do + sign_in(user) + project.team << [user, :developer] + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + end + + it 'redirects to last_page if page number is larger than number of pages' do + get :index, + namespace_id: project.namespace.path.to_param, + project_id: project.path.to_param, + page: (last_page + 1).to_param + + expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope])) + end + + it 'redirects to specified page' do + get :index, + namespace_id: project.namespace.path.to_param, + project_id: project.path.to_param, + page: last_page.to_param + + expect(assigns(:issues).current_page).to eq(last_page) + expect(response).to have_http_status(200) + end + end end describe 'GET #new' do @@ -296,6 +326,20 @@ describe Projects::IssuesController do end describe 'POST #create' do + def post_new_issue(attrs = {}) + sign_in(user) + project = create(:empty_project, :public) + project.team << [user, :developer] + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + issue: { title: 'Title', description: 'Description' }.merge(attrs) + } + + project.issues.first + end + context 'resolving discussions in MergeRequest' do let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } let(:merge_request) { discussion.noteable } @@ -339,13 +383,7 @@ describe Projects::IssuesController do end def post_spam_issue - sign_in(user) - spam_project = create(:empty_project, :public) - post :create, { - namespace_id: spam_project.namespace.to_param, - project_id: spam_project.to_param, - issue: { title: 'Spam Title', description: 'Spam lives here' } - } + post_new_issue(title: 'Spam Title', description: 'Spam lives here') end it 'rejects an issue recognized as spam' do @@ -366,18 +404,26 @@ describe Projects::IssuesController do request.env['action_dispatch.remote_ip'] = '127.0.0.1' end - def post_new_issue + it 'creates a user agent detail' do + expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + end + end + + context 'when description has slash commands' do + before do sign_in(user) - project = create(:empty_project, :public) - post :create, { - namespace_id: project.namespace.to_param, - project_id: project.to_param, - issue: { title: 'Title', description: 'Description' } - } end - it 'creates a user agent detail' do - expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + it 'can add spent time' do + issue = post_new_issue(description: '/spend 1h') + + expect(issue.total_time_spent).to eq(3600) + end + + it 'can set the time estimate' do + issue = post_new_issue(description: '/estimate 2h') + + expect(issue.time_estimate).to eq(7200) end end end diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2ae635a1244b614ab0e4d5849944fccd84f5fe27 --- /dev/null +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Projects::MattermostsController do + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET #new' do + before do + allow_any_instance_of(MattermostSlashCommandsService). + to receive(:list_teams).and_return([]) + + get(:new, + namespace_id: project.namespace.to_param, + project_id: project.to_param) + end + + it 'accepts the request' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #create' do + let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } } + + subject do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + mattermost: mattermost_params) + end + + context 'no request can be made to mattermost' do + it 'shows the error' do + allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"]) + + expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project)) + end + end + + context 'the request is succesull' do + before do + allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token') + end + + it 'redirects to the new page' do + subject + service = project.services.last + + expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service)) + 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 9e0b80205d8f80c4e8d0d2ab6abb6853ee8cc529..7ea3ea4f37689a755d6428bf9d763520f5304b39 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -127,11 +127,29 @@ describe Projects::MergeRequestsController do end describe 'GET index' do - def get_merge_requests + def get_merge_requests(page = nil) get :index, namespace_id: project.namespace.to_param, project_id: project.to_param, - state: 'opened' + state: 'opened', page: page.to_param + end + + context 'when page param' do + let(:last_page) { project.merge_requests.page().total_pages } + let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + + it 'redirects to last_page if page number is larger than number of pages' do + get_merge_requests(last_page + 1) + + expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope])) + end + + it 'redirects to specified page' do + get_merge_requests(last_page) + + expect(assigns(:merge_requests).current_page).to eq(last_page) + expect(response).to have_http_status(200) + end end context 'when filtering by opened state' do @@ -649,10 +667,6 @@ describe Projects::MergeRequestsController do end end - describe 'GET builds' do - it_behaves_like "loads labels", :builds - end - describe 'GET pipelines' do it_behaves_like "loads labels", :pipelines end @@ -1034,4 +1048,72 @@ describe Projects::MergeRequestsController do end end end + + describe 'GET merge_widget_refresh' do + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + format: :raw + } + end + + before do + project.team << [user, :developer] + xhr :get, :merge_widget_refresh, params + end + + context 'when merge in progress' do + let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :success' do + expect(assigns(:status)).to eq(:success) + expect(response).to render_template('merge') + end + end + + context 'when merge request was merged already' do + let(:merge_request) { create(:merge_request, source_project: project, state: :merged) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :success' do + expect(assigns(:status)).to eq(:success) + expect(response).to render_template('merge') + end + end + + context 'when waiting for build' do + let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :merge_when_build_succeeds' do + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(response).to render_template('merge') + end + end + + context 'when no special status for MR' do + let(:merge_request) { create(:merge_request, source_project: project) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to nil' do + expect(assigns(:status)).to be_nil + expect(response).to render_template('merge') + end + end + end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 92e38b02615f7fd75c590cd36ace370db025d46a..9f6d4ec65371306fe896654272f7123412381b2e 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -14,6 +14,54 @@ describe Projects::NotesController do } end + describe 'POST create' do + let(:merge_request) { create(:merge_request) } + let(:request_params) do + { + note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, + namespace_id: project.namespace, + project_id: project, + merge_request_diff_head_sha: 'sha' + } + end + + before do + sign_in(user) + project.team << [user, :developer] + end + + it "returns status 302 for html" do + post :create, request_params + + expect(response).to have_http_status(302) + end + + it "returns status 200 for json" do + post :create, request_params.merge(format: :json) + + expect(response).to have_http_status(200) + end + + context 'when merge_request_diff_head_sha present' do + before do + service_params = { + note: 'some note', + noteable_id: merge_request.id.to_s, + noteable_type: 'MergeRequest', + merge_request_diff_head_sha: 'sha' + } + + expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true)) + end + + it "returns status 302 for html" do + post :create, request_params + + expect(response).to have_http_status(302) + end + end + end + describe 'POST toggle_award_emoji' do before do sign_in(user) diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1ed2ee3ab4afd9f309dd9fc80488f3edb74ca3bf --- /dev/null +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Projects::PipelinesController do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + describe 'GET index.json' do + before do + create_list(:ci_empty_pipeline, 2, project: project) + + get :index, namespace_id: project.namespace.path, + project_id: project.path, + format: :json + end + + it 'returns JSON with serialized pipelines' do + expect(response).to have_http_status(:ok) + + expect(json_response).to include('pipelines') + expect(json_response['pipelines'].count).to eq 2 + expect(json_response['count']['all']).to eq 2 + expect(json_response['count']['running_or_pending']).to eq 2 + end + end + + describe 'GET stages.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when accessing existing stage' do + before do + create(:ci_build, pipeline: pipeline, stage: 'build') + + get_stage('build') + end + + it 'returns html source for stage dropdown' do + expect(response).to have_http_status(:ok) + expect(response).to render_template('projects/pipelines/_stage') + expect(json_response).to include('html') + end + end + + context 'when accessing unknown stage' do + before do + get_stage('test') + end + + it 'responds with not found' do + expect(response).to have_http_status(:not_found) + end + end + + def get_stage(name) + get :stage, namespace_id: project.namespace.path, + project_id: project.path, + id: pipeline.id, + stage: name, + format: :json + end + end +end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index b52137fbe7e595fb2cd1455f6e347b478b5f3cca..442f81187dc2def2a9fc9046154b91f98795e834 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do let(:project) { create(:empty_project, :public, :access_requestable) } describe 'GET index' do - it 'renders index with 200 status code' do + it 'should have the settings/members address with a 302 status code' do get :index, namespace_id: project.namespace, project_id: project - expect(response).to have_http_status(200) - expect(response).to render_template(:index) + expect(response).to have_http_status(302) + expect(response.location).to include namespace_project_settings_members_path(project.namespace, project) end end @@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do access_level: Gitlab::Access::GUEST expect(response).to set_flash.to 'Users were successfully added.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project)) end it 'adds no user to members' do @@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do access_level: Gitlab::Access::GUEST expect(response).to set_flash.to 'No users or groups specified.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project)) end end end @@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do id: member expect(response).to redirect_to( - namespace_project_project_members_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) expect(project.members).not_to include member end @@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do expect(project.team_members).to include member expect(response).to set_flash.to 'Successfully imported' expect(response).to redirect_to( - namespace_project_project_members_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) end end diff --git a/spec/controllers/projects/settings/members_controller_spec.rb b/spec/controllers/projects/settings/members_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..076d6cd9c6ee0b287f33186cda996a1d8423e202 --- /dev/null +++ b/spec/controllers/projects/settings/members_controller_spec.rb @@ -0,0 +1,14 @@ +require('spec_helper') + +describe Projects::Settings::MembersController do + let(:project) { create(:empty_project, :public, :access_requestable) } + + describe 'GET show' do + it 'renders show with 200 status code' do + get :show, namespace_id: project.namespace, project_id: project + + expect(response).to have_http_status(200) + expect(response).to render_template(:show) + end + end +end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 72a3ebf2ebd4cdd2c1517a970a1093267f0822b3..32b0e42c3cd8c7111a4dd73c3fc79b30d00130ea 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -11,6 +11,28 @@ describe Projects::SnippetsController do end describe 'GET #index' do + context 'when page param' do + let(:last_page) { project.snippets.page().total_pages } + let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) } + + it 'redirects to last_page if page number is larger than number of pages' do + get :index, + namespace_id: project.namespace.path, + project_id: project.path, page: (last_page + 1).to_param + + expect(response).to redirect_to(namespace_project_snippets_path(page: last_page)) + end + + it 'redirects to specified page' do + get :index, + namespace_id: project.namespace.path, + project_id: project.path, page: last_page.to_param + + expect(assigns(:snippets).current_page).to eq(last_page) + expect(response).to have_http_status(200) + end + end + context 'when the project snippet is private' do let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 5ddcaa60dc6a39f11ee15c4af557c01fbc11f4e1..d0a63aa9403a5e0220d65daa5ff45cccf5792e54 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -245,7 +245,7 @@ describe ProjectsController do expect(project.repository.path).to include(new_path) expect(assigns(:repository).path).to eq(project.repository.path) - expect(response).to have_http_status(200) + expect(response).to have_http_status(302) end end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 026f41c926bbf422481b0ad372051e5176226727..42fbfe89368cbdb261fdb4001c8682e72fe56307 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -2,30 +2,60 @@ require 'spec_helper' describe RegistrationsController do describe '#create' do - around(:each) do |example| - perform_enqueued_jobs do - example.run + let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } } + + context 'email confirmation' do + around(:each) do |example| + perform_enqueued_jobs do + example.run + end end - end - let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } } + context 'when send_user_confirmation_email is false' do + it 'signs the user in' do + allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) + + expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size } + expect(subject.current_user).not_to be_nil + end + end - context 'when sending email confirmation' do - before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + context 'when send_user_confirmation_email is true' do + it 'does not authenticate user and sends confirmation email' do + allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) - it 'logs user in directly' do - expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size } - expect(subject.current_user).not_to be_nil + post(:create, user_params) + + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(subject.current_user).to be_nil + end end end - context 'when not sending email confirmation' do - before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } + context 'when reCAPTCHA is enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end + + it 'displays an error when the reCAPTCHA is not solved' do + # Without this, `verify_recaptcha` arbitraily returns true in test env + Recaptcha.configuration.skip_verify_env.delete('test') - it 'does not authenticate user and sends confirmation email' do post(:create, user_params) - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) - expect(subject.current_user).to be_nil + + expect(response).to render_template(:new) + expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' + end + + it 'redirects to the dashboard when the recaptcha is solved' do + # Avoid test ordering issue and ensure `verify_recaptcha` returns true + unless Recaptcha.configuration.skip_verify_env.include?('test') + Recaptcha.configuration.skip_verify_env << 'test' + end + + post(:create, user_params) + + expect(flash[:notice]).to include 'Welcome! You have signed up successfully.' end end end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7bb92907125bebf912db24499ac8a2e5abf8a0f --- /dev/null +++ b/spec/controllers/search_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SearchController do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + it 'finds issue comments' do + project = create(:empty_project, :public) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].first).to eq note + end + + context 'on restricted projects' do + context 'when signed out' do + before { sign_out(user) } + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on merge_requests" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on snippets" do + project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_project_snippet, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end +end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..007b35bbb77c6cdb3be5e5fb020bf56c7cf55f55 --- /dev/null +++ b/spec/db/production/settings.rb @@ -0,0 +1,17 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + include StubENV + + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') + end + + it 'writes the token to the database' do + load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) + expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') + end + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index c443af09075536330e401acce0ebfe9368fdad55..0397d5d400185349606de1693b2687d5f2ecae3f 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -12,15 +12,17 @@ FactoryGirl.define do started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' commands 'ls -a' + options do { image: "ruby:2.1", services: ["postgres"] } end + yaml_variables do [ - { key: :DB_NAME, value: 'postgres', public: true } + { key: 'DB_NAME', value: 'postgres', public: true } ] end @@ -60,15 +62,20 @@ FactoryGirl.define do end trait :teardown_environment do - options do - { environment: { action: 'stop' } } - end + environment 'staging' + options environment: { name: 'staging', + action: 'stop' } end trait :allowed_to_fail do allow_failure true end + trait :playable do + skipped + manual + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 1735791f6441bcbde223610b1ddb41979c36fc99..77404f46c92fe2ec1e460aacf887ee661d505b1e 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -31,6 +31,14 @@ FactoryGirl.define do File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) end end + + # Populates pipeline with errors + # + pipeline.config_processor if evaluator.config + end + + trait :invalid do + config(rspec: nil) end end end diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index e3b73e29987514dc89ef26e7f9ae512151bde743..ed4acca23f1e8da3522b4c528703dd29397139c8 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -8,6 +8,10 @@ FactoryGirl.define do is_shared false active true + trait :online do + contacted_at Time.now + end + trait :shared do is_shared true end diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index a81645acd2b86814489de798a3c2d9fb8534a52a..477fab9e964d8c145e48e1032ac74541dbf64dd6 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -2,7 +2,7 @@ include ActionDispatch::TestProcess FactoryGirl.define do factory :lfs_object do - oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" + sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n } size 499013 end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 6919002dedcd439a03f34a9a077916b7272071cd..a10ba6297609638f510f4b8924350aabf842c0fd 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -67,7 +67,7 @@ FactoryGirl.define do end trait :on_project_snippet do - noteable { create(:snippet, project: project) } + noteable { create(:project_snippet, project: project) } end trait :system do diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index da4c72bcb5b32af4b0b41b2ba83b9f2c92ba21ba..811eab7e15bdc298660ec61554b0e667ddb53ddb 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -5,5 +5,6 @@ FactoryGirl.define do name { FFaker::Product.brand } revoked false expires_at { 5.days.from_now } + scopes ['api'] end end diff --git a/spec/factories/project_statistics.rb b/spec/factories/project_statistics.rb new file mode 100644 index 0000000000000000000000000000000000000000..72d4309621629f6b9748afab3ab0341b2ab2ca0a --- /dev/null +++ b/spec/factories/project_statistics.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :project_statistics do + project { create :project } + namespace { project.namespace } + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1166498ddffb165c6806ea3982f695014dd968da..992580a6b344414deb20cb78fd1130b96174ff30 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -24,10 +24,18 @@ FactoryGirl.define do visibility_level Gitlab::VisibilityLevel::PRIVATE end + trait :archived do + archived true + end + trait :access_requestable do request_access_enabled true end + trait :repository do + # no-op... for now! + end + trait :empty_repo do after(:create) do |project| project.create_repository @@ -42,6 +50,12 @@ FactoryGirl.define do end end + trait :test_repo do + after :create do |project| + TestEnv.copy_repo(project) + end + end + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED @@ -91,9 +105,7 @@ FactoryGirl.define do factory :project, parent: :empty_project do path { 'gitlabhq' } - after :create do |project| - TestEnv.copy_repo(project) - end + test_repo end factory :forked_project_with_submodules, parent: :empty_project do @@ -133,4 +145,17 @@ FactoryGirl.define do ) end end + + factory :kubernetes_project, parent: :empty_project do + after :create do |project| + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com', + token: 'a' * 40, + } + ) + end + end end diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb new file mode 100644 index 0000000000000000000000000000000000000000..12fc4ec448634f523fd2be60435b1345d88d0be7 --- /dev/null +++ b/spec/factories/timelogs.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :timelog do + time_spent 3600 + user + association :trackable, factory: :issue + end +end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 866e663f0262ea629387b692a05b94b2072ca54b..082b02116c037cfc155b6ce0a05ae444f3c5f3fb 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -21,6 +21,7 @@ FactoryGirl.define do trait :build_failed do action { Todo::BUILD_FAILED } + target factory: :merge_request end trait :approval_required do diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..16064d60ce2c24d67b6714b30426e2d6735351af --- /dev/null +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +RSpec.describe 'admin active tab' do + before do + login_as :admin + end + + shared_examples 'page has active tab' do |title| + it "activates #{title} tab" do + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) + expect(page.find('.layout-nav li.active')).to have_content(title) + end + end + + shared_examples 'page has active sub tab' do |title| + it "activates #{title} sub tab" do + expect(page).to have_selector('.sub-nav li.active', count: 1) + expect(page.find('.sub-nav li.active')).to have_content(title) + end + end + + context 'on home page' do + before do + visit admin_root_path + end + + it_behaves_like 'page has active tab', 'Overview' + end + + context 'on projects' do + before do + visit admin_projects_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Projects' + end + + context 'on groups' do + before do + visit admin_groups_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Groups' + end + + context 'on users' do + before do + visit admin_users_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Users' + end + + context 'on logs' do + before do + visit admin_logs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Logs' + end + + context 'on messages' do + before do + visit admin_broadcast_messages_path + end + + it_behaves_like 'page has active tab', 'Messages' + end + + context 'on hooks' do + before do + visit admin_hooks_path + end + + it_behaves_like 'page has active tab', 'Hooks' + end + + context 'on background jobs' do + before do + visit admin_background_jobs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Background Jobs' + end +end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..96d715ef383130d7b8e636f352aef233884891a5 --- /dev/null +++ b/spec/features/admin/admin_appearance_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +feature 'Admin Appearance', feature: true do + let!(:appearance) { create(:appearance) } + + scenario 'Create new appearance' do + login_as :admin + visit admin_appearances_path + + fill_in 'appearance_title', with: 'MyCompany' + fill_in 'appearance_description', with: 'dev server' + click_button 'Save' + + expect(current_path).to eq admin_appearances_path + expect(page).to have_content 'Appearance settings' + + expect(page).to have_field('appearance_title', with: 'MyCompany') + expect(page).to have_field('appearance_description', with: 'dev server') + expect(page).to have_content 'Last edit' + end + + scenario 'Preview appearance' do + login_as :admin + + visit admin_appearances_path + click_link "Preview" + + expect_page_has_custom_appearance(appearance) + end + + scenario 'Custom sign-in page' do + visit new_user_session_path + expect_page_has_custom_appearance(appearance) + end + + scenario 'Appearance logo' do + login_as :admin + visit admin_appearances_path + + attach_file(:appearance_logo, logo_fixture) + click_button 'Save' + expect(page).to have_css(logo_selector) + + click_link 'Remove logo' + expect(page).not_to have_css(logo_selector) + end + + scenario 'Header logos' do + login_as :admin + visit admin_appearances_path + + attach_file(:appearance_header_logo, logo_fixture) + click_button 'Save' + expect(page).to have_css(header_logo_selector) + + click_link 'Remove header logo' + expect(page).not_to have_css(header_logo_selector) + end + + def expect_page_has_custom_appearance(appearance) + expect(page).to have_content appearance.title + expect(page).to have_content appearance.description + end + + def logo_selector + '//img[@src^="/uploads/appearance/logo"]' + end + + def header_logo_selector + '//img[@src^="/uploads/appearance/header_logo"]' + end + + def logo_fixture + Rails.root.join('spec', 'fixtures', 'dk.png') + end +end diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bc957ec72e1704d674332772feb081bdea523170 --- /dev/null +++ b/spec/features/admin/admin_broadcast_messages_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +feature 'Admin Broadcast Messages', feature: true do + before do + login_as :admin + create(:broadcast_message, :expired, message: 'Migration to new server') + visit admin_broadcast_messages_path + end + + scenario 'See broadcast messages list' do + expect(page).to have_content 'Migration to new server' + end + + scenario 'Create a customized broadcast message' do + fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**' + fill_in 'broadcast_message_color', with: '#f2dede' + fill_in 'broadcast_message_font', with: '#b94a48' + select Date.today.next_year.year, from: 'broadcast_message_ends_at_1i' + click_button 'Add broadcast message' + + expect(current_path).to eq admin_broadcast_messages_path + expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST' + expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) + end + + scenario 'Edit an existing broadcast message' do + click_link 'Edit' + fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW' + click_button 'Update broadcast message' + + expect(current_path).to eq admin_broadcast_messages_path + expect(page).to have_content 'Application update RIGHT NOW' + end + + scenario 'Remove an existing broadcast message' do + click_link 'Remove' + + expect(current_path).to eq admin_broadcast_messages_path + expect(page).not_to have_content 'Migration to new server' + end + + scenario 'Live preview a customized broadcast message', js: true do + fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:" + + page.within('.broadcast-message-preview') do + expect(page).to have_selector('strong', text: 'Markdown') + expect(page).to have_selector('img.emoji') + end + end +end diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7ce6cce0a5cef4a10c3c67968419641ac1dfd0c9 --- /dev/null +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +RSpec.describe 'admin deploy keys', type: :feature do + let!(:deploy_key) { create(:deploy_key, public: true) } + let!(:another_deploy_key) { create(:another_deploy_key, public: true) } + + before do + login_as(:admin) + end + + it 'show all public deploy keys' do + visit admin_deploy_keys_path + + expect(page).to have_content(deploy_key.title) + expect(page).to have_content(another_deploy_key.title) + end + + describe 'create new deploy key' do + before do + visit admin_deploy_keys_path + click_link 'New Deploy Key' + end + + it 'creates new deploy key' do + fill_deploy_key + click_button 'Create' + + expect_renders_new_key + end + + it 'creates new deploy key with write access' do + fill_deploy_key + check "deploy_key_can_push" + click_button "Create" + + expect_renders_new_key + expect(page).to have_content('Yes') + end + + def expect_renders_new_key + expect(current_path).to eq admin_deploy_keys_path + expect(page).to have_content('laptop') + end + + def fill_deploy_key + fill_in 'deploy_key_title', with: 'laptop' + fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop' + end + end +end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index f6d625fa7f6d172806b0f71310da454024d72db5..a871e370ba2021085669b74e6ee38771c1229201 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -1,15 +1,39 @@ require 'spec_helper' feature 'Admin Groups', feature: true do + include Select2Helper + let(:internal) { Gitlab::VisibilityLevel::INTERNAL } + let(:user) { create :user } + let!(:group) { create :group } + let!(:current_user) { login_as :admin } before do - login_as(:admin) - stub_application_setting(default_group_visibility: internal) end + describe 'list' do + it 'renders groups' do + visit admin_groups_path + + expect(page).to have_content(group.name) + end + end + describe 'create a group' do + it 'creates new group' do + visit admin_groups_path + + click_link "New Group" + fill_in 'group_path', with: 'gitlab' + fill_in 'group_description', with: 'Group description' + click_button "Create group" + + expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab')) + expect(page).to have_content('Group: gitlab') + expect(page).to have_content('Group description') + end + scenario 'shows the visibility level radio populated with the default value' do visit new_admin_group_path @@ -17,16 +41,111 @@ feature 'Admin Groups', feature: true do end end + describe 'show a group' do + scenario 'shows the group' do + group = create(:group, :private) + + visit admin_group_path(group) + + expect(page).to have_content("Group: #{group.name}") + end + end + describe 'group edit' do scenario 'shows the visibility level radio populated with the group visibility_level value' do group = create(:group, :private) - visit edit_admin_group_path(group) + visit admin_group_edit_path(group) expect_selected_visibility(group.visibility_level) end end + describe 'add user into a group', js: true do + shared_context 'adds user into a group' do + it do + visit admin_group_path(group) + + select2(user_selector, from: '#user_ids', multiple: true) + page.within '#new_project_member' do + select2(Gitlab::Access::REPORTER, from: '#access_level') + end + click_button "Add users to group" + page.within ".group-users-list" do + expect(page).to have_content(user.name) + expect(page).to have_content('Reporter') + end + end + end + + it_behaves_like 'adds user into a group' do + let(:user_selector) { user.id } + end + + it_behaves_like 'adds user into a group' do + let(:user_selector) { user.email } + end + end + + describe 'add admin himself to a group' do + before do + group.add_user(:user, Gitlab::Access::OWNER) + end + + it 'adds admin a to a group as developer', js: true do + visit group_group_members_path(group) + + page.within '.users-group-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Add to group' + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + end + + describe 'admin remove himself from a group', js: true do + it 'removes admin from the group' do + group.add_user(current_user, Gitlab::Access::DEVELOPER) + + visit group_group_members_path(group) + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + + visit group_group_members_path(group) + + page.within '.content-list' do + expect(page).not_to have_content(current_user.name) + expect(page).not_to have_content('Developer') + end + end + end + + describe 'shared projects' do + it 'renders shared project' do + empty_project = create(:empty_project) + empty_project.project_group_links.create!( + group_access: Gitlab::Access::MASTER, + group: group + ) + + visit admin_group_path(group) + + expect(page).to have_content(empty_project.name_with_namespace) + expect(page).to have_content('Projects shared with') + end + end + def expect_selected_visibility(level) selector = "#group_visibility_level_#{level}[checked=checked]" diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eaa42aad0a76d906591d14ef069c0e2528edf9f6 --- /dev/null +++ b/spec/features/admin/admin_labels_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +RSpec.describe 'admin issues labels' do + include WaitForAjax + + let!(:bug_label) { Label.create(title: 'bug', template: true) } + let!(:feature_label) { Label.create(title: 'feature', template: true) } + + before do + login_as :admin + end + + describe 'list' do + before do + visit admin_labels_path + end + + it 'renders labels list' do + page.within '.manage-labels-list' do + expect(page).to have_content('bug') + expect(page).to have_content('feature') + end + end + + it 'deletes label' do + page.within "#label_#{bug_label.id}" do + click_link 'Delete' + end + + page.within '.manage-labels-list' do + expect(page).not_to have_content('bug') + end + end + + it 'deletes all labels', js: true do + page.within '.labels' do + page.all('.btn-remove').each do |remove| + wait_for_ajax + remove.click + end + end + + page.within '.manage-labels-list' do + expect(page).not_to have_content('bug') + expect(page).not_to have_content('feature_label') + end + end + end + + describe 'create' do + before do + visit new_admin_label_path + end + + it 'creates new label' do + fill_in 'Title', with: 'support' + fill_in 'Background color', with: '#F95610' + click_button 'Save' + + page.within '.manage-labels-list' do + expect(page).to have_content('support') + end + end + + it 'does not creates label with invalid color' do + fill_in 'Title', with: 'support' + fill_in 'Background color', with: '#12' + click_button 'Save' + + page.within '.label-form' do + expect(page).to have_content('Color must be a valid color code') + end + end + + it 'does not creates label if label already exists' do + fill_in 'Title', with: 'bug' + fill_in 'Background color', with: '#F95610' + click_button 'Save' + + page.within '.label-form' do + expect(page).to have_content 'Title has already been taken' + end + end + end + + describe 'edit' do + it 'changes bug label' do + visit edit_admin_label_path(bug_label) + + fill_in 'Title', with: 'fix' + fill_in 'Background color', with: '#F15610' + click_button 'Save' + + page.within '.manage-labels-list' do + expect(page).to have_content('fix') + end + end + end +end diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c2c618b5659b480c51edb43ca2accc0511ab1a38 --- /dev/null +++ b/spec/features/admin/admin_manage_applications_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +RSpec.describe 'admin manage applications', feature: true do + before do + login_as :admin + end + + it do + visit admin_applications_path + + click_on 'New Application' + expect(page).to have_content('New application') + + fill_in :doorkeeper_application_name, with: 'test' + fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' + click_on 'Submit' + expect(page).to have_content('Application: test') + expect(page).to have_content('Application Id') + expect(page).to have_content('Secret') + + click_on 'Edit' + expect(page).to have_content('Edit application') + + fill_in :doorkeeper_application_name, with: 'test_changed' + click_on 'Submit' + expect(page).to have_content('test_changed') + expect(page).to have_content('Application Id') + expect(page).to have_content('Secret') + + visit admin_applications_path + page.within '.oauth-applications' do + click_on 'Destroy' + end + expect(page.find('.oauth-applications')).not_to have_content('test_changed') + end +end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 30ded9202a4975f16a42a1b0e8503dc192d72517..a5b88812b75594e1b7f75571b3de04ef85e46049 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -1,34 +1,117 @@ require 'spec_helper' describe "Admin::Projects", feature: true do - before do - @project = create(:project) + include Select2Helper + + let(:user) { create :user } + let!(:project) { create(:project) } + let!(:current_user) do login_as :admin end describe "GET /admin/projects" do + let!(:archived_project) { create :project, :public, archived: true } + before do - visit admin_namespaces_projects_path + visit admin_projects_path end it "is ok" do - expect(current_path).to eq(admin_namespaces_projects_path) + expect(current_path).to eq(admin_projects_path) end - it "has projects list" do - expect(page).to have_content(@project.name) + it 'renders projects list without archived project' do + expect(page).to have_content(project.name) + expect(page).not_to have_content(archived_project.name) + end + + it 'renders all projects', js: true do + find(:css, '#sort-projects-dropdown').click + click_link 'Show archived projects' + + expect(page).to have_content(project.name) + expect(page).to have_content(archived_project.name) + expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end end - describe "GET /admin/projects/:id" do + describe "GET /admin/projects/:namespace_id/:id" do before do - visit admin_namespaces_projects_path - click_link "#{@project.name}" + visit admin_projects_path + click_link "#{project.name}" + end + + it do + expect(current_path).to eq admin_namespace_project_path(project.namespace, project) end it "has project info" do - expect(page).to have_content(@project.path) - expect(page).to have_content(@project.name) + expect(page).to have_content(project.path) + expect(page).to have_content(project.name) + expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.creator.name) + end + end + + describe 'transfer project' do + before do + create(:group, name: 'Web') + + allow_any_instance_of(Projects::TransferService). + to receive(:move_uploads_to_new_namespace).and_return(true) + end + + it 'transfers project to group web', js: true do + visit admin_namespace_project_path(project.namespace, project) + + click_button 'Search for Namespace' + click_link 'group: web' + click_button 'Transfer' + + expect(page).to have_content("Web / #{project.name}") + expect(page).to have_content('Namespace: Web') + end + end + + describe 'add admin himself to a project' do + before do + project.team << [user, :master] + end + + it 'adds admin a to a project as developer', js: true do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.users-project-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Add to project' + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + end + + describe 'admin remove himself from a project' do + before do + project.team << [user, :master] + project.team << [current_user, :developer] + end + + it 'removes admin from the project' do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + + expect(page).not_to have_selector(:css, '.content-list') end end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 8cd66f189bec5e018c92feecd20ac0c7689dc172..47fa2f14307d1f35068f6a41bd67cda1ffdcb39b 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -17,9 +17,9 @@ feature 'Admin updates settings', feature: true do expect(page).to have_content "Application settings saved successfully" end - scenario 'Change Slack Service template settings' do + scenario 'Change Slack Notifications Service template settings' do click_link 'Service Templates' - click_link 'Slack' + click_link 'Slack notifications' fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' fill_in 'service_push_channel', with: '#test_channel' @@ -30,7 +30,7 @@ feature 'Admin updates settings', feature: true do expect(page).to have_content 'Application settings saved successfully' - click_link 'Slack' + click_link 'Slack notifications' page.all('input[type=checkbox]').each do |checkbox| expect(checkbox).to be_checked diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index e31325ce47b1fad3e0c4eedb133e7e32fdaf0426..a586f8d31843a61aa88ce8f4bc5328e8b5fe9fa2 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -1,7 +1,13 @@ require 'spec_helper' -describe "Admin::Users", feature: true do - before { login_as :admin } +describe "Admin::Users", feature: true do + include WaitForAjax + + let!(:user) do + create(:omniauth_user, provider: 'twitter', extern_uid: '123456') + end + + let!(:current_user) { login_as :admin } describe "GET /admin/users" do before do @@ -13,8 +19,10 @@ describe "Admin::Users", feature: true do end it "has users list" do - expect(page).to have_content(@user.email) - expect(page).to have_content(@user.name) + expect(page).to have_content(current_user.email) + expect(page).to have_content(current_user.name) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) end describe 'Two-factor Authentication filters' do @@ -38,8 +46,6 @@ describe "Admin::Users", feature: true do end it 'counts users who have not enabled 2FA' do - create(:user) - visit admin_users_path page.within('.filter-two-factor-disabled small') do @@ -48,8 +54,6 @@ describe "Admin::Users", feature: true do end it 'filters by users who have not enabled 2FA' do - user = create(:user) - visit admin_users_path click_link '2FA Disabled' @@ -108,10 +112,10 @@ describe "Admin::Users", feature: true do describe "GET /admin/users/:id" do it "has user info" do visit admin_users_path - click_link @user.name + click_link user.name - expect(page).to have_content(@user.email) - expect(page).to have_content(@user.name) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) end describe 'Impersonation' do @@ -124,7 +128,7 @@ describe "Admin::Users", feature: true do end it 'does not show impersonate button for admin itself' do - visit admin_user_path(@user) + visit admin_user_path(current_user) expect(page).not_to have_content('Impersonate') end @@ -156,7 +160,7 @@ describe "Admin::Users", feature: true do it 'logs out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username) + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do @@ -169,15 +173,15 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication status' do it 'shows when enabled' do - @user.update_attribute(:otp_required_for_login, true) + user.update_attribute(:otp_required_for_login, true) - visit admin_user_path(@user) + visit admin_user_path(user) expect_two_factor_status('Enabled') end it 'shows when disabled' do - visit admin_user_path(@user) + visit admin_user_path(user) expect_two_factor_status('Disabled') end @@ -192,9 +196,8 @@ describe "Admin::Users", feature: true do describe "GET /admin/users/:id/edit" do before do - @simple_user = create(:user) visit admin_users_path - click_link "edit_user_#{@simple_user.id}" + click_link "edit_user_#{user.id}" end it "has user edit page" do @@ -212,45 +215,168 @@ describe "Admin::Users", feature: true do click_button "Save changes" end - it "shows page with new data" do + it "shows page with new data" do expect(page).to have_content('bigbang@mail.com') expect(page).to have_content('Big Bang') end it "changes user entry" do - @simple_user.reload - expect(@simple_user.name).to eq('Big Bang') - expect(@simple_user.is_admin?).to be_truthy - expect(@simple_user.password_expires_at).to be <= Time.now + user.reload + expect(user.name).to eq('Big Bang') + expect(user.is_admin?).to be_truthy + expect(user.password_expires_at).to be <= Time.now + end + end + + describe 'update username to non ascii char' do + it do + fill_in 'user_username', with: '\u3042\u3044' + click_button('Save') + + page.within '#error_explanation' do + expect(page).to have_content('Username') + end + + expect(page).to have_selector(%(form[action="/admin/users/#{user.username}"])) end end end describe "GET /admin/users/:id/projects" do + let(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + before do - @group = create(:group) - @project = create(:project, group: @group) - @simple_user = create(:user) - @group.add_developer(@simple_user) + group.add_developer(user) - visit projects_admin_user_path(@simple_user) + visit projects_admin_user_path(user) end it "lists group projects" do within(:css, '.append-bottom-default + .panel') do expect(page).to have_content 'Group projects' - expect(page).to have_link @group.name, admin_group_path(@group) + expect(page).to have_link group.name, admin_group_path(group) end end it 'allows navigation to the group details' do within(:css, '.append-bottom-default + .panel') do - click_link @group.name + click_link group.name end within(:css, 'h3.page-title') do - expect(page).to have_content "Group: #{@group.name}" + expect(page).to have_content "Group: #{group.name}" + end + expect(page).to have_content project.name + end + + it 'shows the group access level' do + within(:css, '.append-bottom-default + .panel') do + expect(page).to have_content 'Developer' end - expect(page).to have_content @project.name + end + + it 'allows group membership to be revoked', js: true do + page.within(first('.group_member')) do + find('.btn-remove').click + end + wait_for_ajax + + expect(page).not_to have_selector('.group_member') + end + end + + describe 'show user attributes' do + it do + visit admin_users_path + + click_link user.name + + expect(page).to have_content 'Account' + expect(page).to have_content 'Personal projects limit' + end + end + + describe 'remove users secondary email', js: true do + let!(:secondary_email) do + create :email, email: 'secondary@example.com', user: user + end + + it do + visit admin_user_path(user.username) + + expect(page).to have_content("Secondary email: #{secondary_email.email}") + + find("#remove_email_#{secondary_email.id}").click + + expect(page).not_to have_content(secondary_email.email) + end + end + + describe 'show user keys' do + let!(:key1) do + create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1") + end + + let!(:key2) do + create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2") + end + + it do + visit admin_users_path + + click_link user.name + click_link 'SSH keys' + + expect(page).to have_content(key1.title) + expect(page).to have_content(key2.title) + + click_link key2.title + + expect(page).to have_content(key2.title) + expect(page).to have_content(key2.key) + + click_link 'Remove' + + expect(page).not_to have_content(key2.title) + end + end + + describe 'show user identities' do + it 'shows user identities' do + visit admin_user_identities_path(user) + + expect(page).to have_content(user.name) + expect(page).to have_content('twitter') + end + end + + describe 'update user identities' do + before do + allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) + end + + it 'modifies twitter identity' do + visit admin_user_identities_path(user) + + find('.table').find(:link, 'Edit').click + fill_in 'identity_extern_uid', with: '654321' + select 'twitter_updated', from: 'identity_provider' + click_button 'Save changes' + + expect(page).to have_content(user.name) + expect(page).to have_content('twitter_updated') + expect(page).to have_content('654321') + end + end + + describe 'remove user with identities' do + it 'removes user with twitter identity' do + visit admin_user_identities_path(user) + + click_link 'Delete' + + expect(page).to have_content(user.name) + expect(page).not_to have_content('twitter') end end end diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ea7a97d1d4fde10f29b239c665c38756112b7461 --- /dev/null +++ b/spec/features/auto_deploy_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe 'Auto deploy' do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com', + token: 'a' * 40, + } + ) + project.team << [user, :master] + login_as user + end + + context 'when no deployment service is active' do + before do + project.kubernetes_service.update!(active: false) + end + + it 'does not show a button to set up auto deploy' do + visit namespace_project_path(project.namespace, project) + expect(page).to have_no_content('Set up auto deploy') + end + end + + context 'when a deployment service is active' do + before do + project.kubernetes_service.update!(active: true) + visit namespace_project_path(project.namespace, project) + end + + it 'shows a button to set up auto deploy' do + expect(page).to have_link('Set up auto deploy') + end + + it 'includes OpenShift as an available template', js: true do + click_link 'Set up auto deploy' + click_button 'Choose a GitLab CI Yaml template' + + within '.gitlab-ci-yml-selector' do + expect(page).to have_content('OpenShift') + end + end + + it 'creates a merge request using "auto-deploy" branch', js: true do + click_link 'Set up auto deploy' + click_button 'Choose a GitLab CI Yaml template' + within '.gitlab-ci-yml-selector' do + click_on 'OpenShift' + end + wait_for_ajax + click_button 'Commit Changes' + + expect(page).to have_content('New Merge Request From auto-deploy into master') + end + end +end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 973d5b286e95f0bc7afaee3f5d151777e75be5c6..bfac5a1b8ab288f4e9990039bc5aca8cddb5e2b6 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -109,7 +109,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'search backlog list' do - page.within('#js-boards-seach') do + page.within('#js-boards-search') do find('.form-control').set(issue1.title) end @@ -122,7 +122,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'search done list' do - page.within('#js-boards-seach') do + page.within('#js-boards-search') do find('.form-control').set(issue8.title) end @@ -135,7 +135,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'search list' do - page.within('#js-boards-seach') do + page.within('#js-boards-search') do find('.form-control').set(issue5.title) end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c16aafa14704db7306232cd7ac31b30297bcc0bb..188d33e8ef4338e1127cb6df1f627dd6553af5c0 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -125,7 +125,9 @@ describe 'Issue Boards', feature: true, js: true do first('.card').click end - page.within('.assignee') do + page.within(find('.assignee')) do + expect(page).to have_content('No assignee') + click_link 'assign yourself' wait_for_vue_resource diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index 81077f4b00547fc52506a4e1f82239907388a3a3..3ebc432206a0049e9dc12cca291b2f583cf6d467 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'CI Lint' do +describe 'CI Lint', js: true do before do login_as :user end @@ -8,7 +8,10 @@ describe 'CI Lint' do describe 'YAML parsing' do before do visit ci_lint_path - fill_in 'content', with: yaml_content + # Ace editor updates a hidden textarea and it happens asynchronously + # `sleep 0.1` is actually needed here because of this + execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");") + sleep 0.1 click_on 'Validate' end @@ -40,7 +43,7 @@ describe 'CI Lint' do let(:yaml_content) { 'my yaml content' } it 'loads previous YAML content after validation' do - expect(page).to have_field('content', with: 'my yaml content', type: 'textarea') + expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea') end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 23a504ff965cbcd7bc158e7f3790942b0e9bac0b..8f561c8f90b4a398cb3c47c589bbf61299559442 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -107,7 +107,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do visit ci_status_path(pipeline) - click_on 'Cancel' + find('a.btn[title="Cancel"]').click expect(page).to have_content 'canceled' end end diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0648c89a5c7383e0423350c395cfc54b105d29ff --- /dev/null +++ b/spec/features/cycle_analytics_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +feature 'Cycle Analytics', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:guest) { create(:user) } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } + let(:milestone) { create(:milestone, project: project) } + let(:mr) { create_merge_request_closing_issue(issue) } + let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha) } + + context 'as an allowed user' do + context 'when project is new' do + before do + project.team << [user, :master] + login_as(user) + visit namespace_project_cycle_analytics_path(project.namespace, project) + wait_for_ajax + end + + it 'shows introductory message' do + expect(page).to have_content('Introducing Cycle Analytics') + end + + it 'shows active stage with empty message' do + expect(page).to have_selector('.stage-nav-item.active', text: 'Issue') + expect(page).to have_content("We don't have enough data to show this stage.") + end + end + + context "when there's cycle analytics data" do + before do + project.team << [user, :master] + + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) + create_cycle + deploy_master + + login_as(user) + visit namespace_project_cycle_analytics_path(project.namespace, project) + end + + it 'shows data on each stage' do + expect_issue_to_be_present + + click_stage('Plan') + expect(find('.stage-events')).to have_content(mr.commits.last.title) + + click_stage('Code') + expect_merge_request_to_be_present + + click_stage('Test') + expect_build_to_be_present + + click_stage('Review') + expect_merge_request_to_be_present + + click_stage('Staging') + expect_build_to_be_present + + click_stage('Production') + expect_issue_to_be_present + end + end + end + + context "as a guest" do + before do + project.team << [guest, :guest] + + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) + create_cycle + deploy_master + + login_as(guest) + visit namespace_project_cycle_analytics_path(project.namespace, project) + wait_for_ajax + end + + it 'needs permissions to see restricted stages' do + expect(find('.stage-events')).to have_content(issue.title) + + click_stage('Code') + expect(find('.stage-events')).to have_content('You need permission.') + + click_stage('Review') + expect(find('.stage-events')).to have_content('You need permission.') + end + end + + def expect_issue_to_be_present + expect(find('.stage-events')).to have_content(issue.title) + expect(find('.stage-events')).to have_content(issue.author.name) + expect(find('.stage-events')).to have_content("##{issue.iid}") + end + + def expect_build_to_be_present + expect(find('.stage-events')).to have_content(@build.ref) + expect(find('.stage-events')).to have_content(@build.short_sha) + expect(find('.stage-events')).to have_content("##{@build.id}") + end + + def expect_merge_request_to_be_present + expect(find('.stage-events')).to have_content(mr.title) + expect(find('.stage-events')).to have_content(mr.author.name) + expect(find('.stage-events')).to have_content("!#{mr.iid}") + end + + def create_cycle + issue.update(milestone: milestone) + pipeline.run + + @build = create(:ci_build, pipeline: pipeline, status: :success, author: user) + + merge_merge_requests_closing_issue(issue) + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) + end + + def click_stage(stage_name) + find('.stage-nav li', text: stage_name).click + wait_for_ajax + end +end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7d59fcac517dbdffcf4804841f2b15e6ae338ec3 --- /dev/null +++ b/spec/features/dashboard/active_tab_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Active Tab', feature: true do + before do + login_as :user + end + + shared_examples 'page has active tab' do |title| + it "#{title} tab" do + expect(page).to have_selector('.nav-sidebar li.active', count: 1) + expect(find('.nav-sidebar li.active')).to have_content(title) + end + end + + context 'on dashboard projects' do + before do + visit dashboard_projects_path + end + + it_behaves_like 'page has active tab', 'Projects' + end + + context 'on dashboard issues' do + before do + visit issues_dashboard_path + end + + it_behaves_like 'page has active tab', 'Issues' + end + + context 'on dashboard merge requests' do + before do + visit merge_requests_dashboard_path + end + + it_behaves_like 'page has active tab', 'Merge Requests' + end + + context 'on dashboard groups' do + before do + visit dashboard_groups_path + end + + it_behaves_like 'page has active tab', 'Groups' + end +end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..038c1641be9cacd664f84aa9c85de77267517077 --- /dev/null +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Archived Project', feature: true do + let(:user) { create :user } + let(:project) { create :project} + let(:archived_project) { create(:project, :archived) } + + before do + project.team << [user, :master] + archived_project.team << [user, :master] + + login_as(user) + + visit dashboard_projects_path + end + + it 'renders non archived projects' do + expect(page).to have_link(project.name) + expect(page).not_to have_link(archived_project.name) + end + + it 'renders all projects' do + click_link 'Show archived projects' + + expect(page).to have_link(project.name) + expect(page).to have_link(archived_project.name) + end +end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 44dfc2dff450b502495603a6877b2d93a7c20bf7..dc9d09fa396c54845f7d6d2fd2ac77445579c473 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -6,7 +6,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:created_date) { Date.yesterday.to_time } - let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') } + let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P') } context 'on the activity tab' do before do diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d5f8470fab039100a222346819ef38967fa549ad --- /dev/null +++ b/spec/features/dashboard/group_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Group', feature: true do + before do + login_as(:user) + end + + it 'creates new grpup' do + visit dashboard_groups_path + click_link 'New Group' + + fill_in 'group_path', with: 'Samurai' + fill_in 'group_description', with: 'Tokugawa Shogunate' + click_button 'Create group' + + expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) + expect(page).to have_content('Samurai') + expect(page).to have_content('Tokugawa Shogunate') + end +end diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2803f7ec62b1a907d909d1d14ab509f9f67aa733 --- /dev/null +++ b/spec/features/dashboard/help_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Help', feature: true do + before do + login_as(:user) + end + + it 'renders correctly markdown' do + visit help_page_path("administration/raketasks/maintenance") + + expect(page).to have_content('Gather information about GitLab and the system it runs on') + + node = find('.documentation h2 a#user-content-check-gitlab-configuration') + expect(node[:href]).to eq '#check-gitlab-configuration' + expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + end +end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 0c1939fd885da18ddcaa8030310017f3834aa5ce..56f6cd2e095be70b81fb784afe1c81a324f74812 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -38,6 +38,10 @@ feature 'Environment', :feature do scenario 'does not show a re-deploy button for deployment without build' do expect(page).not_to have_link('Re-deploy') end + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end end context 'with related deployable present' do @@ -60,6 +64,10 @@ feature 'Environment', :feature do expect(page).not_to have_link('Stop') end + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } @@ -84,6 +92,26 @@ feature 'Environment', :feature do end end + context 'with terminal' do + let(:project) { create(:kubernetes_project, :test_repo) } + + context 'for project master' do + let(:role) { :master } + + scenario 'it shows the terminal button' do + expect(page).to have_terminal_button + end + end + + context 'for developer' do + let(:role) { :developer } + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + end + end + context 'with stop action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } @@ -158,4 +186,8 @@ feature 'Environment', :feature do environment.project, environment) end + + def have_terminal_button + have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) + end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index e1b97b31e5d7b5e485281406270c7e234834cf52..72b984cfab849a5edf61c8bc0e616d275307f32c 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -113,6 +113,10 @@ feature 'Environments page', :feature, :js do expect(page).not_to have_css('external-url') end + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } @@ -145,6 +149,26 @@ feature 'Environments page', :feature, :js do end end end + + context 'with terminal' do + let(:project) { create(:kubernetes_project, :test_repo) } + + context 'for project master' do + let(:role) { :master } + + scenario 'it shows the terminal button' do + expect(page).to have_terminal_button + end + end + + context 'for developer' do + let(:role) { :developer } + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + end + end end end end @@ -195,6 +219,10 @@ feature 'Environments page', :feature, :js do end end + def have_terminal_button + have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) + end + def visit_environments(project) visit namespace_project_environments_path(project.namespace, project) end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 3934c936f20e8dec46b9ff5589a708742d6de1a2..8b3e2fa93a2aed4a164f9a8ddff9641ae36f414c 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -4,10 +4,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do include WaitForAjax let(:branch) { 'expand-collapse-diffs' } + let(:project) { create(:project) } before do login_as :admin - project = create(:project) # Ensure that undiffable.md is in .gitattributes project.repository.copy_gitattributes(branch) @@ -31,6 +31,33 @@ feature 'Expand and collapse diffs', js: true, feature: true do define_method(file.split('.').first) { file_container(file) } end + it 'should show the diff content with a highlighted line when linking to line' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + + visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: "#{large_diff[:id]}_0_1") + execute_script('window.location.reload()') + + wait_for_ajax + + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + expect(large_diff).to have_selector('.hll') + end + + it 'should show the diff content when linking to file' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + + visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: large_diff[:id]) + execute_script('window.location.reload()') + + wait_for_ajax + + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + context 'visiting a commit with collapsed diffs' do it 'shows small diffs immediately' do expect(small_diff).to have_selector('.code') diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..608aedd34717088c4ad74cf3a172f4051bbd1a67 --- /dev/null +++ b/spec/features/groups/members/sorting_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +feature 'Groups > Members > Sorting', feature: true do + let(:owner) { create(:user, name: 'John Doe') } + let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } + let(:group) { create(:group) } + + background do + create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago) + create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago) + + login_as(owner) + end + + scenario 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') + end + + scenario 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') + end + + scenario 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') + end + + scenario 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') + end + + scenario 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') + end + + scenario 'sorts by recent sign in' do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') + end + + scenario 'sorts by oldest sign in' do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') + end + + def visit_members_list(sort:) + visit group_group_members_path(group.to_param, sort: sort) + end + + def first_member + page.all('ul.content-list > li').first.text + end + + def second_member + page.all('ul.content-list > li').last.text + end +end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 4b19886274e0e537faa5818b255914c70ede767d..a515c92db3795da7a8ba8031a2a054d2a71614ea 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -107,4 +107,17 @@ feature 'Group', feature: true do expect(page).to have_css('.group-home-desc a[rel]') end end + + describe 'group page with nested groups', js: true do + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:path) { group_path(group) } + + it 'has nested groups tab with nested groups inside' do + visit path + click_link 'Subgroups' + + expect(page).to have_content(nested_group.full_name) + end + end end diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index 9a2b879e789a97f19bf2b2ef240016428be6b774..73553f97d6f399ee20a11137dabe9e33c96b95bc 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -180,16 +180,10 @@ describe 'Projects > Issuables > Default sort order', feature: true do end def visit_merge_requests_with_state(project, state) - visit_merge_requests project - visit_issuables_with_state state + visit_merge_requests project, state: state end def visit_issues_with_state(project, state) - visit_issues project - visit_issuables_with_state state - end - - def visit_issuables_with_state(state) - within('.issues-state-filters') { find("span", text: state.titleize).click } + visit_issues project, state: state end end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index efb530264492f60c7e52f4caa4c1c607bb92e82b..73e43316dc7ff8eafe0fa45b46f1589dface9668 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -76,7 +76,7 @@ describe 'Awards Emoji', feature: true do end it 'has disabled emoji button' do - expect(first('.award-control')[:disabled]).to be(true) + expect(first('.award-control')[:class]).to have_text('disabled') end end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index bc2c087c9b9a0f44ebc0a2608c716baaf36e92e5..832757b24d4f6fd71dee14fbf6346ec67e5e61cf 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do let!(:issue2) { create(:issue, project: project, title: "Issue 2") } let!(:bug) { create(:label, project: project, title: 'bug') } let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:wontfix) { create(:label, project: project, title: 'wontfix') } context 'as an allowed user', js: true do before do @@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' end end + + # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877 + context 'unmarking common label' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'applies label from filtered results' do + check 'check_all_issues' + + page.within('.issues_bulk_update') do + click_button 'Labels' + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') + + click_link 'bug' + find('.dropdown-input-field', visible: true).set('wontfix') + click_link 'wontfix' + end + + update_issues + + page.within '.issues-holder' do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue1.id}")).to have_content 'wontfix' + + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'wontfix' + end + end + end end context 'as a guest' do @@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do - click_button 'Label' + click_button 'Labels' wait_for_ajax items.map do |item| click_link item diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb deleted file mode 100644 index 9dfa5d1de1991eb543acc69bbd3023dd4ce14d39..0000000000000000000000000000000000000000 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rails_helper' - -feature 'Issue filtering by Milestone', feature: true do - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - - scenario 'filters by no Milestone', js: true do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::None.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') - expect(page).to have_css('.issue', count: 1) - end - - context 'filters by upcoming milestone', js: true do - it 'does not show issues with no expiry' do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - - it 'shows issues in future' do - milestone = create(:milestone, project: project, due_date: Date.tomorrow) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 1) - end - - it 'does not show issues in past' do - milestone = create(:milestone, project: project, due_date: Date.yesterday) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - - context 'when milestone has single quotes in title' do - background do - milestone.update(name: "rock 'n' roll") - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - end - - def visit_issues(project) - visit namespace_project_issues_path(project.namespace, project) - end - - def filter_by_milestone(title) - find(".js-milestone-select").click - find(".milestone-filter .dropdown-content a", text: title).click - end -end diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6f6a2532c04c19f3ba3786a9faceeacdf0c874f1 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -0,0 +1,166 @@ +require 'rails_helper' + +describe 'Dropdown assignee', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_assignee) { '#js-dropdown-assignee' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 5 + wait_for_ajax + end + end + + def dropdown_assignee_size + page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size + end + + def click_assignee(text) + find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has assignee:' do + filtered_search.set('assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_assignee, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('assignee:') + + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('assignee:') + + expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') + end + + it 'should load all the assignees when opened' do + send_keys_to_filtered_search('assignee:') + + expect(dropdown_assignee_size).to eq(3) + end + end + + describe 'filtering' do + before do + send_keys_to_filtered_search('assignee:') + end + + it 'filters by name' do + send_keys_to_filtered_search('j') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('J') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username with symbol' do + send_keys_to_filtered_search('@OT') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + + expect(dropdown_assignee_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('assignee:') + end + + it 'fills in the assignee username when the assignee has not been filtered' do + click_assignee(user_jacob.name) + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") + end + + it 'fills in the assignee username when the assignee has been filtered' do + send_keys_to_filtered_search('roo') + click_assignee(user.name) + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user.username}") + end + + it 'selects `no assignee`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:none") + end + end + + describe 'input has existing content' do + it 'opens assignee dropdown with existing search term' do + filtered_search.set('searchTerm assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing author' do + filtered_search.set('author:@user assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing label' do + filtered_search.set('label:~bug assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..60a86cc93d42c4434bba0a7a0b92ba1e02a24a26 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -0,0 +1,154 @@ +require 'rails_helper' + +describe 'Dropdown author', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_author) { '#js-dropdown-author' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 5 + wait_for_ajax + end + end + + def dropdown_author_size + page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size + end + + def click_author(text) + find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has author:' do + filtered_search.set('author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_author, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('author:') + + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('author:') + + expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') + end + + it 'should load all the authors when opened' do + send_keys_to_filtered_search('author:') + + expect(dropdown_author_size).to eq(3) + end + end + + describe 'filtering' do + before do + filtered_search.set('author') + send_keys_to_filtered_search(':') + end + + it 'filters by name' do + send_keys_to_filtered_search('ja') + + expect(dropdown_author_size).to eq(1) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('Ja') + + expect(dropdown_author_size).to eq(1) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + + expect(dropdown_author_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + + expect(dropdown_author_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('author') + send_keys_to_filtered_search(':') + end + + it 'fills in the author username when the author has not been filtered' do + click_author(user_jacob.name) + + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user_jacob.username}") + end + + it 'fills in the author username when the author has been filtered' do + click_author(user.name) + + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user.username}") + end + end + + describe 'input has existing content' do + it 'opens author dropdown with existing search term' do + filtered_search.set('searchTerm author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing assignee' do + filtered_search.set('assignee:@user author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing label' do + filtered_search.set('label:~bug author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..04dd54ab45937e632640cd0ab815daab5baedec7 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -0,0 +1,134 @@ +require 'rails_helper' + +describe 'Dropdown hint', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_hint) { '#js-dropdown-hint' } + + def dropdown_hint_size + page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + end + + def click_hint(text) + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + before do + expect(page).to have_css(js_dropdown_hint, visible: false) + filtered_search.click + end + + it 'opens when the search bar is first focused' do + expect(page).to have_css(js_dropdown_hint, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click + + expect(page).to have_css(js_dropdown_hint, visible: false) + end + end + + describe 'filtering' do + it 'does not filter `Keep typing and press Enter`' do + filtered_search.set('randomtext') + + expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) + expect(dropdown_hint_size).to eq(0) + end + + it 'filters with text' do + filtered_search.set('a') + + expect(dropdown_hint_size).to eq(3) + end + end + + describe 'selecting from dropdown with no input' do + before do + filtered_search.click + end + + it 'opens the author dropdown when you click on author' do + click_hint('author') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + click_hint('assignee') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + click_hint('milestone') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + click_hint('label') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end + + describe 'selecting from dropdown with some input' do + it 'opens the author dropdown when you click on author' do + filtered_search.set('auth') + click_hint('author') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + filtered_search.set('assign') + click_hint('assignee') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + filtered_search.set('mile') + click_hint('milestone') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + filtered_search.set('lab') + click_hint('label') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..89c144141c9366dbd80165554dbcef2fb00e0792 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -0,0 +1,242 @@ +require 'rails_helper' + +describe 'Dropdown label', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } + let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } + let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') } + let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} + let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_label) { '#js-dropdown-label' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_label_size + page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size + end + + def click_label(text) + find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has label:' do + filtered_search.set('label:') + + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_label, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('label:') + + expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('label:') + + expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') + end + + it 'should load all the labels when opened' do + send_keys_to_filtered_search('label:') + + expect(dropdown_label_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('label') + end + + it 'filters by name' do + send_keys_to_filtered_search(':b') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search(':B') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search(':~bu') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search(':~BU') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by multiple words' do + send_keys_to_filtered_search(':Hig') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words with symbol' do + send_keys_to_filtered_search(':~Hig') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing single quotes' do + send_keys_to_filtered_search(':won\'t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing single quotes with symbol' do + send_keys_to_filtered_search(':~won\'t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing double quotes' do + send_keys_to_filtered_search(':won"t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing double quotes with symbol' do + send_keys_to_filtered_search(':~won"t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search(':^+') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search(':~^+') + + expect(dropdown_label_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('label:') + end + + it 'fills in the label name when the label has not been filled' do + click_label(bug_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name when the label is partially filled' do + send_keys_to_filtered_search('bu') + click_label(bug_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name that contains multiple words' do + click_label(two_words_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") + end + + it 'fills in the label name that contains multiple words and is very long' do + click_label(long_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") + end + + it 'fills in the label name that contains double quotes' do + click_label(wont_fix_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") + end + + it 'fills in the label name with the correct capitalization' do + click_label(uppercase_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") + end + + it 'fills in the label name with special characters' do + click_label(special_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{special_label.title}") + end + + it 'selects `no label`' do + find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:none") + end + end + + describe 'input has existing content' do + it 'opens label dropdown with existing search term' do + filtered_search.set('searchTerm label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing author' do + filtered_search.set('author:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing assignee' do + filtered_search.set('assignee:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing label' do + filtered_search.set('label:~urgent label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing milestone' do + filtered_search.set('milestone:%v2.0 label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e5a271b663f186a6c4b7053e2365aeb31bdb4538 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -0,0 +1,222 @@ +require 'rails_helper' + +describe 'Dropdown milestone', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:milestone) { create(:milestone, title: 'v1.0', project: project) } + let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) } + let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) } + let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) } + let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) } + let!(:long_milestone) { create(:milestone, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title', project: project) } + + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_milestone) { '#js-dropdown-milestone' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_milestone_size + page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size + end + + def click_milestone(text) + find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click + end + + def click_static_milestone(text) + find('#js-dropdown-milestone .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has milestone:' do + filtered_search.set('milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_milestone, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('milestone:') + + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('milestone:') + + expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') + end + + it 'should load all the milestones when opened' do + send_keys_to_filtered_search('milestone:') + + expect(dropdown_milestone_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('milestone') + end + + it 'filters by name' do + send_keys_to_filtered_search(':v1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search(':V1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search(':%v1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search(':%V1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search(':(+') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search(':%(+') + + expect(dropdown_milestone_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('milestone:') + end + + it 'fills in the milestone name when the milestone has not been filled' do + click_milestone(milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name when the milestone is partially filled' do + send_keys_to_filtered_search('v') + click_milestone(milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name that contains multiple words' do + click_milestone(two_words_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") + end + + it 'fills in the milestone name that contains multiple words and is very long' do + click_milestone(long_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") + end + + it 'fills in the milestone name that contains double quotes' do + click_milestone(wont_fix_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") + end + + it 'fills in the milestone name with the correct capitalization' do + click_milestone(uppercase_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") + end + + it 'fills in the milestone name with special characters' do + click_milestone(special_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") + end + + it 'selects `no milestone`' do + click_static_milestone('No Milestone') + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:none") + end + + it 'selects `upcoming milestone`' do + click_static_milestone('Upcoming') + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:upcoming") + end + end + + describe 'input has existing content' do + it 'opens milestone dropdown with existing search term' do + filtered_search.set('searchTerm milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing author' do + filtered_search.set('author:@john milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing assignee' do + filtered_search.set('assignee:@john milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing label' do + filtered_search.set('label:~important milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing milestone' do + filtered_search.set('milestone:%100 milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ead43d6784a3378e7b458aeed8ade8e7c7a7f2d3 --- /dev/null +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -0,0 +1,759 @@ +require 'rails_helper' + +describe 'Filter issues', js: true, feature: true do + include WaitForAjax + + let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } + let!(:milestone) { create(:milestone, title: "8", project: project) } + let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } + + let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } + let(:filtered_search) { find('.filtered-search') } + + def input_filtered_search(search_term) + filtered_search.set(search_term) + filtered_search.send_keys(:enter) + end + + def expect_filtered_search_input(input) + expect(find('.filtered-search').value).to eq(input) + end + + def expect_no_issues_list + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + + def expect_issues_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: open_count) + end + end + + before do + project.team << [user, :master] + project.team << [user2, :master] + group.add_developer(user) + group.add_developer(user2) + login_as(user) + create(:issue, project: project) + + create(:issue, title: "Bug report 1", project: project) + create(:issue, title: "Bug report 2", project: project) + create(:issue, title: "issue with 'single quotes'", project: project) + create(:issue, title: "issue with \"double quotes\"", project: project) + create(:issue, title: "issue with !@\#{$%^&*()-+", project: project) + create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user) + create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_caps_label.labels << caps_sensitive_label + + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_everything.labels << bug_label + issue_with_everything.labels << caps_sensitive_label + + multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) + multiple_words_label_issue.labels << multiple_words_label + + future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) + + create(:issue, + title: "Issue with future milestone", + milestone: future_milestone, + project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'filter issues by author' do + context 'only author' do + it 'filters issues by searched author' do + input_filtered_search("author:@#{user.username}") + + expect_issues_list_count(5) + end + + it 'filters issues by invalid author' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple authors' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + + context 'author with other filters' do + it 'filters issues by searched author and text' do + search = "author:@#{user.username} issue" + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee and text' do + search = "author:@#{user.username} assignee:@#{user.username} issue" + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee, label, and text' do + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee, label, milestone and text' do + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + it 'sorting' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + + describe 'filter issues by assignee' do + context 'only assignee' do + it 'filters issues by searched assignee' do + search = "assignee:@#{user.username}" + input_filtered_search(search) + + expect_issues_list_count(5) + expect_filtered_search_input(search) + end + + it 'filters issues by no assignee' do + search = "assignee:none" + input_filtered_search(search) + + expect_issues_list_count(8, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by invalid assignee' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple assignees' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + + context 'assignee with other filters' do + it 'filters issues by searched assignee and text' do + search = "assignee:@#{user.username} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author and text' do + search = "assignee:@#{user.username} author:@#{user.username} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author, label, text' do + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author, label, milestone and text' do + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'sorting' do + it 'sorts' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + end + + describe 'filter issues by label' do + context 'only label' do + it 'filters issues by searched label' do + search = "label:~#{bug_label.title}" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by no label' do + search = "label:none" + input_filtered_search(search) + + expect_issues_list_count(9, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by invalid label' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple labels' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by label containing special characters' do + special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') + special_issue = create(:issue, title: "Issue with special character label", project: project) + special_issue.labels << special_label + + search = "label:~#{special_label.title}" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show issues' do + new_label = create(:label, project: project, title: "new_label") + + search = "label:~#{new_label.title}" + input_filtered_search(search) + + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'label with multiple words' do + it 'special characters' do + special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") + special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) + special_multiple_issue.labels << special_multiple_label + + search = "label:~'#{special_multiple_label.title}'" + input_filtered_search(search) + + expect_issues_list_count(1) + + # filtered search defaults quotations to double quotes + expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") + end + + it 'single quotes' do + search = "label:~'#{multiple_words_label.title}'" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") + end + + it 'double quotes' do + search = "label:~\"#{multiple_words_label.title}\"" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'single quotes containing double quotes' do + double_quotes_label = create(:label, project: project, title: 'won"t fix') + double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + double_quotes_label_issue.labels << double_quotes_label + + search = "label:~'#{double_quotes_label.title}'" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'double quotes containing single quotes' do + single_quotes_label = create(:label, project: project, title: "won't fix") + single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) + single_quotes_label_issue.labels << single_quotes_label + + search = "label:~\"#{single_quotes_label.title}\"" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'label with other filters' do + it 'filters issues by searched label and text' do + search = "label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author, assignee and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author, assignee, milestone and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'multiple labels with other filters' do + it 'filters issues by searched label, label2, and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author, assignee and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author, assignee, milestone and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'issue label clicked' do + before do + find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + sleep 1 + end + + it 'filters' do + expect_issues_list_count(1) + end + + it 'displays in search bar' do + expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + end + end + + context 'sorting' do + it 'sorts' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + end + + describe 'filter issues by milestone' do + context 'only milestone' do + it 'filters issues by searched milestone' do + input_filtered_search("milestone:%#{milestone.title}") + + expect_issues_list_count(5) + end + + it 'filters issues by no milestone' do + input_filtered_search("milestone:none") + + expect_issues_list_count(7, 1) + end + + it 'filters issues by upcoming milestones' do + input_filtered_search("milestone:upcoming") + + expect_issues_list_count(1) + end + + it 'filters issues by invalid milestones' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple milestones' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by milestone containing special characters' do + special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) + create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) + + search = "milestone:%#{special_milestone.title}" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show issues' do + new_milestone = create(:milestone, title: "new", project: project) + + search = "milestone:%#{new_milestone.title}" + input_filtered_search(search) + + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'milestone with other filters' do + it 'filters issues by searched milestone and text' do + search = "milestone:%#{milestone.title} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author, assignee and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author, assignee, label and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + end + + context 'sorting' do + it 'sorts' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + end + + describe 'filter issues by text' do + context 'only text' do + it 'filters issues by searched text' do + search = 'Bug' + input_filtered_search(search) + + expect_issues_list_count(4, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by multiple searched text' do + search = 'Bug report' + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by case insensitive searched text' do + search = 'bug report' + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing single quotes' do + search = '\'single quotes\'' + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing double quotes' do + search = '"double quotes"' + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing special characters' do + search = '!@#{$%^&*()-+' + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show any issues' do + search = 'testing' + input_filtered_search(search) + + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'searched text with other filters' do + it 'filters issues by searched text and author' do + input_filtered_search("bug author:@#{user.username}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} bug") + end + + it 'filters issues by searched text, author and more text' do + input_filtered_search("bug author:@#{user.username} report") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} bug report") + end + + it 'filters issues by searched text, author and assignee' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") + end + + it 'filters issues by searched text, author, more text and assignee' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") + end + + it 'filters issues by searched text, author, more text, assignee and even more text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") + end + + it 'filters issues by searched text, author, assignee and label' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") + end + + it 'filters issues by searched text, author, assignee, label and milestone' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") + end + + it 'filters issues by searched text, author, assignee, multiple labels and milestone' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") + end + end + + context 'sorting' do + it 'sorts by oldest updated' do + create(:issue, + title: '3 days ago', + project: project, + author: user, + created_at: 3.days.ago, + updated_at: 3.days.ago) + + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, + created_at: 5.days.ago, + updated_at: 5.days.ago) + + input_filtered_search('days ago') + + expect_issues_list_count(2) + + sort_toggle = find('.filtered-search-container .dropdown-toggle') + sort_toggle.click + + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click + wait_for_ajax + + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) + end + end + end + + describe 'retains filter when switching issue states' do + before do + input_filtered_search('bug') + + # Wait for search results to load + sleep 2 + end + + it 'open state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + find('.issues-state-filters a', text: 'Open').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 4) + end + + it 'closed state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 1) + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) + end + + it 'all state' do + find('.issues-state-filters a', text: 'All').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 5) + end + end + + describe 'RSS feeds' do + it 'updates atom feed link for project issues' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end + + it 'updates atom feed link for group issues' do + visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end + end +end diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..56b1d354eb015f245c7d16d851d79819e4bedd65 --- /dev/null +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +describe 'Search bar', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + def get_left_style(style) + left_style = /left:\s\d*[.]\d*px/.match(style) + left_style.to_s.gsub('left: ', '').to_f + end + + describe 'clear search button' do + it 'clears text' do + search_text = 'search_text' + filtered_search.set(search_text) + + expect(filtered_search.value).to eq(search_text) + find('.filtered-search-input-container .clear-search').click + + expect(filtered_search.value).to eq('') + end + + it 'hides by default' do + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides after clicked' do + filtered_search.set('a') + find('.filtered-search-input-container .clear-search').click + + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides when there is no text' do + filtered_search.set('a') + filtered_search.set('') + + expect(page).to have_css('.clear-search', visible: false) + end + + it 'shows when there is text' do + filtered_search.set('a') + + expect(page).to have_css('.clear-search', visible: true) + end + + it 'resets the dropdown hint filter' do + filtered_search.click + original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + + filtered_search.set('author') + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) + end + + it 'resets the dropdown filters' do + filtered_search.set('a') + hint_style = page.find('#js-dropdown-hint')['style'] + hint_offset = get_left_style(hint_style) + + filtered_search.set('author:') + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 + expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset) + end + end +end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index cd0512a37e66e443b1438ad499b3153412e4fbcb..82c9bd0e6e6b988063bdf54253bbadd0f88afeb6 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -39,7 +39,6 @@ feature 'GFM autocomplete', feature: true, js: true do page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("~#{label.title[0]}") - sleep 1 note.click end @@ -48,12 +47,52 @@ feature 'GFM autocomplete', feature: true, js: true do expect_to_wrap(true, label_item, note, label.title) end + it "shows dropdown after a new line" do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('test') + note.native.send_keys(:enter) + note.native.send_keys(:enter) + note.native.send_keys('@') + end + + expect(page).to have_selector('.atwho-container') + end + + it "does not show dropdown when preceded with a special character" do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys("@") + note.click + end + + expect(page).to have_selector('.atwho-container') + + page.within '.timeline-content-form' do + note.native.send_keys("@") + note.click + end + + expect(page).to have_selector('.atwho-container', visible: false) + end + + it "does not throw an error if no labels exist" do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys('~') + note.click + end + + expect(page).to have_selector('.atwho-container', visible: false) + end + it 'doesn\'t wrap for assignee values' do note = find('#note_note') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("@#{user.username[0]}") - sleep 1 note.click end @@ -67,7 +106,6 @@ feature 'GFM autocomplete', feature: true, js: true do page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys(":cartwheel") - sleep 1 note.click end @@ -76,6 +114,22 @@ feature 'GFM autocomplete', feature: true, js: true do expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1') end + it 'doesn\'t open autocomplete after non-word character' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("@#{user.username[0..2]}!") + end + + expect(page).not_to have_selector('.atwho-view') + end + + it 'doesn\'t open autocomplete if there is no space before' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("hello:#{user.username[0..2]}") + end + + expect(page).not_to have_selector('.atwho-view') + end + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c8c9c50396bee009cac0cff4f60023488a7692bb --- /dev/null +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +feature 'Issue markdown toolbar', feature: true, js: true do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + before do + login_as(user) + + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it "doesn't include first new line when adding bold" do + find('#note_note').native.send_keys('test') + find('#note_note').native.send_key(:enter) + find('#note_note').native.send_keys('bold') + + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)') + + first('.toolbar-btn').click + + expect(find('#note_note')[:value]).to eq("test\n**bold**\n") + end + + it "doesn't include first new line when adding underline" do + find('#note_note').native.send_keys('test') + find('#note_note').native.send_key(:enter) + find('#note_note').native.send_keys('underline') + + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)') + + find('.toolbar-btn:nth-child(2)').click + + expect(find('#note_note')[:value]).to eq("test\n*underline*\n") + end +end diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb deleted file mode 100644 index c9a3ecf16ea1ae528bbb456eba4a80647f6d339e..0000000000000000000000000000000000000000 --- a/spec/features/issues/reset_filters_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'rails_helper' - -feature 'Issues filter reset button', feature: true, js: true do - include WaitForAjax - include IssueHelpers - - let!(:project) { create(:project, :public) } - let!(:user) { create(:user)} - let!(:milestone) { create(:milestone, project: project) } - let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')} - let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')} - - before do - project.team << [user, :developer] - end - - context 'when a milestone filter has been applied' do - it 'resets the milestone filter' do - visit_issues(project, milestone_title: milestone.title) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a label filter has been applied' do - it 'resets the label filter' do - visit_issues(project, label_name: bug.name) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a text search has been conducted' do - it 'resets the text search filter' do - visit_issues(project, search: 'Bug') - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when author filter has been applied' do - it 'resets the author filter' do - visit_issues(project, author_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when assignee filter has been applied' do - it 'resets the assignee filter' do - visit_issues(project, assignee_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when all filters have been applied' do - it 'resets all filters' do - visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') - expect(page).to have_css('.issue', count: 0) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when no filters have been applied' do - it 'the reset link should not be visible' do - visit_issues(project) - expect(page).to have_css('.issue', count: 2) - expect(page).not_to have_css '.reset_filters' - end - end - - def reset_filters - find('.reset-filters').click - end -end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 3f2da1c380c0e0c13d11f03a21c54effe3df58cf..0a9cd11ad6e524eaae8d15a67217cba40dd4503f 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -30,7 +30,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do write_note("/due 2016-08-28") expect(page).not_to have_content '/due 2016-08-28' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' issue.reload @@ -51,7 +51,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do write_note("/due 2016-08-28") expect(page).to have_content '/due 2016-08-28' - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Commands applied' issue.reload @@ -70,7 +70,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do write_note("/remove_due_date") expect(page).not_to have_content '/remove_due_date' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' issue.reload @@ -91,7 +91,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do write_note("/remove_due_date") expect(page).to have_content '/remove_due_date' - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Commands applied' issue.reload @@ -100,6 +100,58 @@ feature 'Issues > User uses slash commands', feature: true, js: true do end end + describe 'Issuable time tracking' do + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :developer] + end + + context 'Issue' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'issuable time tracker' + end + + context 'Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it_behaves_like 'issuable time tracker' + end + end + + describe 'Issuable time tracking' do + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :developer] + end + + context 'Issue' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'issuable time tracker' + end + + context 'Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it_behaves_like 'issuable time tracker' + end + end + describe 'toggling the WIP prefix from the title from note' do let(:issue) { create(:issue, project: project) } diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 5c95845560475b7cbfdca14202ab92038c17ad5c..394eb31aff86c88ed339d95d090dc376e0b354aa 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -371,23 +371,25 @@ describe 'Issues', feature: true do describe 'when I want to reset my incoming email token' do let(:project1) { create(:project, namespace: @user.namespace) } - let(:issue) { create(:issue, project: project1) } + let!(:issue) { create(:issue, project: project1) } before do - allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") project1.team << [@user, :master] - project1.issues << issue visit namespace_project_issues_path(@user.namespace, project1) end it 'changes incoming email address token', js: true do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value - find('.incoming-email-token-reset').click - wait_for_ajax - expect(find('input#issue_email').value).not_to eq(previous_token) + expect(page).to have_no_field('issue_email', with: previous_token) + new_token = project1.new_issue_address(@user.reload) + expect(page).to have_field( + 'issue_email', + with: new_token + ) end end @@ -617,14 +619,18 @@ describe 'Issues', feature: true do end it 'adds due date to issue' do + date = Date.today.at_beginning_of_month + 2.days + page.within '.due_date' do click_link 'Edit' page.within '.ui-datepicker-calendar' do - first('.ui-state-default').click + click_link date.day end - expect(page).to have_no_content 'None' + wait_for_ajax + + expect(find('.value').text).to have_content date.strftime('%b %-d, %Y') end end @@ -636,6 +642,8 @@ describe 'Issues', feature: true do first('.ui-state-default').click end + wait_for_ajax + expect(page).to have_no_content 'No due date' click_link 'remove due date' diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c73065cdce1d5870c6456d012b2bfe4cedc52007 --- /dev/null +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature 'Merge Request closing issues message', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue_1) { create(:issue, project: project)} + let(:issue_2) { create(:issue, project: project)} + let(:merge_request) do + create( + :merge_request, + :simple, + source_project: project, + description: merge_request_description + ) + end + let(:merge_request_description) { 'Merge Request Description' } + + before do + project.team << [user, :master] + + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'not closing or mentioning any issue' do + it 'does not display closing issue message' do + expect(page).not_to have_css('.mr-widget-footer') + end + end + + context 'closing issues but not mentioning any other issue' do + let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}") + end + end + + context 'mentioning issues but not closing them' do + let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") + end + end + + context 'closing some issues and mentioning, but not closing, others' do + let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") + end + end +end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 142649297cc8342f4eb84009f693ba5e1e9f7206..73c5ef31edc5513d65d40522459efcc258259918 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -54,14 +54,14 @@ feature 'Merge request created from fork' do scenario 'user visits a pipelines page', js: true do visit_merge_request(merge_request) - page.within('.merge-request-tabs') { click_link 'Builds' } + page.within('.merge-request-tabs') { click_link 'Pipelines' } page.within('table.ci-table') do - expect(page).to have_content 'rspec' - expect(page).to have_content 'spinach' + expect(page).to have_content pipeline.status + expect(page).to have_content pipeline.id end - expect(find_link('Cancel running')[:href]) + expect(page.find('a.btn-remove')[:href]) .to include fork_project.path_with_namespace end end diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index d5c9ed8a3b7fe863f7cabc83dddcccd1f7bfc76e..0952b17b63ec057508b1910a102c36d25a729596 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' # message to be shown by JavaScript when the source branch was deleted. # Please do not remove "js: true". describe 'Deleted source branch', feature: true, js: true do + include WaitForAjax + let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } @@ -13,7 +15,8 @@ describe 'Deleted source branch', feature: true, js: true do merge_request.update!(source_branch: 'this-branch-does-not-exist') visit namespace_project_merge_request_path( merge_request.project.namespace, - merge_request.project, merge_request + merge_request.project, + merge_request ) end @@ -23,11 +26,17 @@ describe 'Deleted source branch', feature: true, js: true do ) end - it 'hides Discussion, Commits and Changes tabs' do + it 'still contains Discussion, Commits and Changes tabs' do within '.merge-request-details' do - expect(page).to have_no_content('Discussion') - expect(page).to have_no_content('Commits') - expect(page).to have_no_content('Changes') + expect(page).to have_content('Discussion') + expect(page).to have_content('Commits') + expect(page).to have_content('Changes') end + + click_on 'Changes' + wait_for_ajax + + expect(page).to have_selector('.diffs.tab-pane .nothing-here-block') + expect(page).to have_content('Nothing to merge from this-branch-does-not-exist into feature') end end diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index c9a0059645d99f2010d226e7b154993e5271c0c8..4a6c76a5caf906d9182f7e5f951c5be8db2aac41 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -22,4 +22,18 @@ feature 'Diffs URL', js: true, feature: true do expect(page).to have_css('.diffs.tab-pane.active') end end + + context 'when merge request has overflow' do + it 'displays warning' do + allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true) + allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20) + + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + + page.within('.alert') do + expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve + performance only 3 of 3+ files are displayed.") + end + end + end end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb similarity index 83% rename from spec/features/issues/filter_by_labels_spec.rb rename to spec/features/merge_requests/filter_by_labels_spec.rb index 0253629f753fb03602b897f5af04af09f74913e8..4c60329865cf7c64ec2937202331ca43096004ea 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -7,25 +7,27 @@ feature 'Issue filtering by Labels', feature: true, js: true do let!(:user) { create(:user) } let!(:label) { create(:label, project: project) } - before do - bug = create(:label, project: project, title: 'bug') - feature = create(:label, project: project, title: 'feature') - enhancement = create(:label, project: project, title: 'enhancement') + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:enhancement) { create(:label, project: project, title: 'enhancement') } + + let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } + let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } + let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } - issue1 = create(:issue, title: "Bugfix1", project: project) - issue1.labels << bug + before do + mr1.labels << bug - issue2 = create(:issue, title: "Bugfix2", project: project) - issue2.labels << bug - issue2.labels << enhancement + mr2.labels << bug + mr2.labels << enhancement - issue3 = create(:issue, title: "Feature1", project: project) - issue3.labels << feature + mr3.title = "Feature1" + mr3.labels << feature project.team << [user, :master] login_as(user) - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) end context 'filter by label bug' do diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb similarity index 66% rename from spec/features/issues/filter_issues_spec.rb rename to spec/features/merge_requests/filter_merge_requests_spec.rb index 0d19563d6284e9048226b8657cb4aaa29e0f30aa..4642b5a530d1c2ec3b0913dd089ca63b9945a81f 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -1,10 +1,10 @@ require 'rails_helper' -describe 'Filter issues', feature: true do +describe 'Filter merge requests', feature: true do include WaitForAjax + let!(:project) { create(:project) } let!(:group) { create(:group) } - let!(:project) { create(:project, group: group) } let!(:user) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } @@ -14,12 +14,12 @@ describe 'Filter issues', feature: true do project.team << [user, :master] group.add_developer(user) login_as(user) - create(:issue, project: project) + create(:merge_request, source_project: project, target_project: project) end - describe 'for assignee from issues#index' do + describe 'for assignee from mr#index' do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-assignee-search').click @@ -47,9 +47,9 @@ describe 'Filter issues', feature: true do end end - describe 'for milestone from issues#index' do + describe 'for milestone from mr#index' do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-milestone-select').click @@ -77,9 +77,9 @@ describe 'Filter issues', feature: true do end end - describe 'for label from issues#index', js: true do + describe 'for label from mr#index', js: true do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-label-select').click wait_for_ajax end @@ -127,7 +127,7 @@ describe 'Filter issues', feature: true do expect(page).to have_content wontfix.title end - find('.dropdown-menu-close-icon').click + find('body').click expect(find('.filtered-labels')).to have_content(wontfix.title) @@ -135,7 +135,7 @@ describe 'Filter issues', feature: true do wait_for_ajax find('.dropdown-menu-labels a', text: label.title).click - find('.dropdown-menu-close-icon').click + find('body').click expect(find('.filtered-labels')).to have_content(wontfix.title) expect(find('.filtered-labels')).to have_content(label.title) @@ -150,21 +150,21 @@ describe 'Filter issues', feature: true do it "selects and unselects `won't fix`" do find('.dropdown-menu-labels a', text: wontfix.title).click find('.dropdown-menu-labels a', text: wontfix.title).click - - find('.dropdown-menu-close-icon').click + # Close label dropdown to load + find('body').click expect(page).not_to have_css('.filtered-labels') end end describe 'for assignee and label from issues#index' do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-assignee-search').click find('.dropdown-menu-user-link', text: user.username).click - expect(page).not_to have_selector('.issues-list .issue') + expect(page).not_to have_selector('.mr-list .merge-request') find('.js-label-select').click @@ -196,38 +196,40 @@ describe 'Filter issues', feature: true do end end - describe 'filter issues by text' do + describe 'filter merge requests by text' do before do - create(:issue, title: "Bug", project: project) + create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") bug_label = create(:label, project: project, title: 'bug') milestone = create(:milestone, title: "8", project: project) - issue = create(:issue, - title: "Bug 2", - project: project, + mr = create(:merge_request, + title: "Bug 2", + source_project: project, + target_project: project, + source_branch: "bug2", milestone: milestone, author: user, assignee: user) - issue.labels << bug_label + mr.labels << bug_label - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) end context 'only text', js: true do - it 'filters issues by searched text' do + it 'filters merge requests by searched text' do fill_in 'issuable_search', with: 'Bug' - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end end - it 'does not show any issues' do + it 'does not show any merge requests' do fill_in 'issuable_search', with: 'testing' - page.within '.issues-list' do - expect(page).not_to have_selector('.issue') + page.within '.mr-list' do + expect(page).not_to have_selector('.merge-request') end end end @@ -237,8 +239,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Label' @@ -248,8 +250,8 @@ describe 'Filter issues', feature: true do find('.dropdown-menu-close-icon').click expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end @@ -257,8 +259,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Milestone' @@ -267,8 +269,8 @@ describe 'Filter issues', feature: true do end expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end @@ -276,8 +278,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Assignee' @@ -286,8 +288,8 @@ describe 'Filter issues', feature: true do end expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end @@ -295,8 +297,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Author' @@ -305,26 +307,27 @@ describe 'Filter issues', feature: true do end expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end end end - describe 'filter issues and sort', js: true do + describe 'filter merge requests and sort', js: true do before do bug_label = create(:label, project: project, title: 'bug') - bug_one = create(:issue, title: "Frontend", project: project) - bug_two = create(:issue, title: "Bug 2", project: project) - bug_one.labels << bug_label - bug_two.labels << bug_label + mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") + mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") - visit namespace_project_issues_path(project.namespace, project) + mr1.labels << bug_label + mr2.labels << bug_label + + visit namespace_project_merge_requests_path(project.namespace, project) end - it 'is able to filter and sort issues' do + it 'is able to filter and sort merge requests' do click_button 'Label' wait_for_ajax page.within '.labels-filter' do @@ -334,8 +337,8 @@ describe 'Filter issues', feature: true do wait_for_ajax expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Last created' @@ -344,41 +347,9 @@ describe 'Filter issues', feature: true do end wait_for_ajax - page.within '.issues-list' do + page.within '.mr-list' do expect(page).to have_content('Frontend') end end end - - it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) - - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end - - it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: '', assignee_id: user.id) - - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end end diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3dbe26cddb0fda2c9ed2bb7c258d6e17e0dc4635 --- /dev/null +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +feature 'Clicking toggle commit message link', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue_1) { create(:issue, project: project)} + let(:issue_2) { create(:issue, project: project)} + let(:merge_request) do + create( + :merge_request, + :simple, + source_project: project, + description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" + ) + end + let(:textbox) { page.find(:css, '.js-commit-message', visible: false) } + let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) } + let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) } + let(:default_message) do + [ + "Merge branch 'feature' into 'master'", + merge_request.title, + "Closes #{issue_1.to_reference} and #{issue_2.to_reference}", + "See merge request #{merge_request.to_reference}" + ].join("\n\n") + end + let(:message_with_description) do + [ + "Merge branch 'feature' into 'master'", + merge_request.title, + merge_request.description, + "See merge request #{merge_request.to_reference}" + ].join("\n\n") + end + + before do + project.team << [user, :master] + + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(textbox).not_to be_visible + click_link "Modify commit message" + expect(textbox).to be_visible + end + + it "toggles commit message between message with description and without description " do + expect(textbox.value).to eq(default_message) + + click_link "Include description in commit message" + + expect(textbox.value).to eq(message_with_description) + + click_link "Don't include description in commit message" + + expect(textbox.value).to eq(default_message) + end + + it "toggles link between 'Include description' and 'Don't include description'" do + expect(include_link).to be_visible + expect(do_not_include_link).not_to be_visible + + click_link "Include description in commit message" + + expect(include_link).not_to be_visible + expect(do_not_include_link).to be_visible + + click_link "Don't include description in commit message" + + expect(include_link).to be_visible + expect(do_not_include_link).not_to be_visible + end +end diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb index 09451f41de4241bb69182d865e8a147fc4c407ca..04e85ed3f73c2c7406565c8d61cd41a9992868ce 100644 --- a/spec/features/merge_requests/merge_request_versions_spec.rb +++ b/spec/features/merge_requests/merge_request_versions_spec.rb @@ -24,7 +24,7 @@ feature 'Merge Request versions', js: true, feature: true do before do page.within '.mr-version-dropdown' do find('.btn-default').click - click_link 'version 1' + find(:link, 'version 1').trigger('click') end end @@ -45,7 +45,7 @@ feature 'Merge Request versions', js: true, feature: true do before do page.within '.mr-version-compare-dropdown' do find('.btn-default').click - click_link 'version 1' + find(:link, 'version 1').trigger('click') end end @@ -81,4 +81,52 @@ feature 'Merge Request versions', js: true, feature: true do expect(page).to have_content '8 changed files' end end + + describe 'compare with same version' do + before do + page.within '.mr-version-compare-dropdown' do + find('.btn-default').click + click_link 'version 1' + end + end + + it 'should have 0 chages between versions' do + page.within '.mr-version-compare-dropdown' do + expect(page).to have_content 'version 1' + end + + page.within '.mr-version-dropdown' do + find('.btn-default').click + find(:link, 'version 1').trigger('click') + end + + expect(page).to have_content '0 changed files' + end + end + + describe 'compare with newer version' do + before do + page.within '.mr-version-compare-dropdown' do + find('.btn-default').click + click_link 'version 2' + end + end + + it 'should set the compared versions to be the same' do + page.within '.mr-version-compare-dropdown' do + expect(page).to have_content 'version 2' + end + + page.within '.mr-version-dropdown' do + find('.btn-default').click + find(:link, 'version 1').trigger('click') + end + + page.within '.mr-version-compare-dropdown' do + expect(page).to have_content 'version 1' + end + + expect(page).to have_content '0 changed files' + end + end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3a7ece7e1d6de16f317b72320c8fba9f5080f3a9 --- /dev/null +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +feature 'Issues filter reset button', feature: true, js: true do + include WaitForAjax + include IssueHelpers + + let!(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} + let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } + let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + + let(:merge_request_css) { '.merge-request' } + + before do + mr2.labels << bug + project.team << [user, :developer] + end + + context 'when a milestone filter has been applied' do + it 'resets the milestone filter' do + visit_merge_requests(project, milestone_title: milestone.title) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a label filter has been applied' do + it 'resets the label filter' do + visit_merge_requests(project, label_name: bug.name) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a text search has been conducted' do + it 'resets the text search filter' do + visit_merge_requests(project, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when author filter has been applied' do + it 'resets the author filter' do + visit_merge_requests(project, author_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when assignee filter has been applied' do + it 'resets the assignee filter' do + visit_merge_requests(project, assignee_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when all filters have been applied' do + it 'resets all filters' do + visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 0) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when no filters have been applied' do + it 'the reset link should not be visible' do + visit_merge_requests(project) + expect(page).to have_css(merge_request_css, count: 2) + expect(page).not_to have_css '.reset_filters' + end + end + + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def reset_filters + find('.reset-filters').click + end +end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 7b8af555f0e3bf7ede508646cd90e9caef3bab6d..b13674b4db9fca8d25caffe9703a067c35916339 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -31,7 +31,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do write_note("/wip") expect(page).not_to have_content '/wip' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(merge_request.reload.work_in_progress?).to eq true end @@ -42,7 +42,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do write_note("/wip") expect(page).not_to have_content '/wip' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(merge_request.reload.work_in_progress?).to eq false end @@ -61,13 +61,58 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do write_note("/wip") expect(page).not_to have_content '/wip' - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Commands applied' expect(merge_request.reload.work_in_progress?).to eq false end end end + describe 'merging the MR from the note' do + context 'when the current user can merge the MR' do + it 'merges the MR' do + write_note("/merge") + + expect(page).to have_content 'Commands applied' + + expect(merge_request.reload).to be_merged + end + end + + context 'when the head diff changes in the meanwhile' do + before do + merge_request.source_branch = 'another_branch' + merge_request.save + end + + it 'does not merge the MR' do + write_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + + context 'when the current user cannot merge the MR' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not merge the MR' do + write_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + end + describe 'adding a due date from note' do it 'does not recognize the command nor create a note' do write_note('/due 2016-08-28') diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb index 8b603f51545bbce278082ca7bdb25a4b64ac6eea..aadd72a9f8e1ace7b303a70b3ccbd62708035831 100644 --- a/spec/features/milestones/milestones_spec.rb +++ b/spec/features/milestones/milestones_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'Milestone draggable', feature: true, js: true do + include WaitForAjax + let(:milestone) { create(:milestone, project: project, title: 8.14) } let(:project) { create(:empty_project, :public) } let(:user) { create(:user) } @@ -74,6 +76,8 @@ describe 'Milestone draggable', feature: true, js: true do visit namespace_project_milestone_path(project.namespace, project, milestone) issue.drag_to(issue_target) + + wait_for_ajax end def create_and_drag_merge_request(params = {}) @@ -82,5 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do visit namespace_project_milestone_path(project.namespace, project, milestone) page.find("a[href='#tab-merge-requests']").click merge_request.drag_to(merge_request_target) + + wait_for_ajax end end diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..40b4dc6369734f7ae35d422dc369e75c2c9fdf73 --- /dev/null +++ b/spec/features/milestones/show_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe 'Milestone show', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:milestone) { create(:milestone, project: project) } + let(:labels) { create_list(:label, 2, project: project) } + let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } } + + before do + project.add_user(user, :developer) + login_as(user) + end + + def visit_milestone + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + it 'avoids N+1 database queries' do + create(:labeled_issue, issue_params) + control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count + create_list(:labeled_issue, 10, issue_params) + + expect { visit_milestone }.not_to exceed_query_limit(control_count) + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 9fffbb43e87e493f6912de64b00dfa5ef57d2d70..b785b2f7704a580c9e4387f0a4f5bd5685ff71bb 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -70,16 +70,15 @@ describe 'Comments', feature: true do end describe 'when editing a note', js: true do - it 'contains the hidden edit form' do - page.within("#note_#{note.id}") do - is_expected.to have_css('.note-edit-form', visible: false) - end + it 'there should be a hidden edit form' do + is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1) + is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1) end describe 'editing the note' do before do find('.note').hover - find(".js-note-edit").click + find('.js-note-edit').click end it 'shows the note edit form and hide the note body' do @@ -90,14 +89,29 @@ describe 'Comments', feature: true do end end - # TODO: fix after 7.7 release - # it "should reset the edit note form textarea with the original content of the note if cancelled" do - # within(".current-note-edit-form") do - # fill_in "note[note]", with: "Some new content" - # find(".btn-cancel").click - # expect(find(".js-note-text", visible: false).text).to eq note.note - # end - # end + it 'should reset the edit note form textarea with the original content of the note if cancelled' do + within('.current-note-edit-form') do + fill_in 'note[note]', with: 'Some new content' + find('.btn-cancel').click + expect(find('.js-note-text', visible: false).text).to eq '' + end + end + + it 'allows using markdown buttons after saving a note and then trying to edit it again' do + page.within('.current-note-edit-form') do + fill_in 'note[note]', with: 'This is the new content' + find('.btn-save').click + end + + find('.note').hover + find('.js-note-edit').click + + page.within('.current-note-edit-form') do + expect(find('#note_note').value).to eq('This is the new content') + find('.js-md:first-child').click + expect(find('#note_note').value).to eq('This is the new content****') + end + end it 'appends the edited at time to the note' do page.within('.current-note-edit-form') do diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index a78a1c9c8905ca1721c7026df7707a4765961e40..c2545b0c2591e11574ba08e5a5936bb40d82b162 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Member autocomplete', feature: true do + include WaitForAjax + let(:project) { create(:project, :public) } let(:user) { create(:user) } let(:participant) { create(:user) } @@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do end def open_member_suggestions - sleep 1 page.within('.new-note') do - sleep 1 - find('#note_note').native.send_keys('@') + find('#note_note').send_keys('@') end + wait_for_ajax end def visit_issue(project, issue) diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index a85930c75434b0b629106c2ff8b1b33ddb29a82e..55a01057c83b25b1d7acea05b2033e3d3563f955 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -27,28 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do describe "token creation" do it "allows creation of a token" do - visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand - - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) - expect(active_personal_access_tokens).to have_text("Never") - end + name = FFaker::Product.brand - it "allows creation of a token with an expiry date" do visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand + fill_in "Name", with: name # Set date to 1st of next month find_field("Expires at").trigger('focus') find("a[title='Next']").click click_on "1" - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) + # Scopes + check "api" + check "read_user" + + click_on "Create Personal Access Token" + expect(active_personal_access_tokens).to have_text(name) expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) + expect(active_personal_access_tokens).to have_text('api') + expect(active_personal_access_tokens).to have_text('read_user') end context "when creation fails" do @@ -85,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do disallow_personal_access_token_saves! visit profile_personal_access_tokens_path - expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count } + click_on "Revoke" expect(active_personal_access_tokens).to have_text(personal_access_token.name) expect(page).to have_content("Could not revoke") end diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 8c4d4320dc5c83e9027bd7d7878457b0c5dd22f0..11d27feab0b22232598254a6955456c579a05938 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -3,6 +3,7 @@ require 'tempfile' feature 'Builds', :feature do let(:user) { create(:user) } + let(:user_access_level) { :developer } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -14,7 +15,7 @@ feature 'Builds', :feature do end before do - project.team << [user, :developer] + project.team << [user, user_access_level] login_as(user) end @@ -131,7 +132,9 @@ feature 'Builds', :feature do context 'Artifacts expire date' do before do - build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at) + build.update_attributes(artifacts_file: artifacts_file, + artifacts_expire_at: expire_at) + visit namespace_project_build_path(project.namespace, project, build) end @@ -146,12 +149,23 @@ feature 'Builds', :feature do context 'when expire date is defined' do let(:expire_at) { Time.now + 7.days } - it 'keeps artifacts when Keep button is clicked' do - expect(page).to have_content 'The artifacts will be removed' - click_link 'Keep' + context 'when user has ability to update build' do + it 'keeps artifacts when keep button is clicked' do + expect(page).to have_content 'The artifacts will be removed' - expect(page).not_to have_link 'Keep' - expect(page).not_to have_content 'The artifacts will be removed' + click_link 'Keep' + + expect(page).to have_no_link 'Keep' + expect(page).to have_no_content 'The artifacts will be removed' + end + end + + context 'when user does not have ability to update build' do + let(:user_access_level) { :guest } + + it 'does not have keep button' do + expect(page).to have_no_link 'Keep' + end end end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index fcdf7870f34f2ffd889a50e4406ecb9df44b5323..33f1c323af17e0ed6251d93675954e490aadb6ed 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project commit builds' do +feature 'project commit pipelines' do given(:project) { create(:project) } background do @@ -16,11 +16,13 @@ feature 'project commit builds' do ref: 'master') end - scenario 'user views commit builds page' do - visit builds_namespace_project_commit_path(project.namespace, - project, project.commit.sha) + scenario 'user views commit pipelines page' do + visit pipelines_namespace_project_commit_path(project.namespace, project, project.commit.sha) - expect(page).to have_content('Builds') + page.within('.table-holder') do + expect(page).to have_content project.pipelines[0].status # pipeline status + expect(page).to have_content project.pipelines[0].id # pipeline ids + end end end end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 3bb33394be7a678529003bbbe30c14b5ec636a21..9079350186def349fcaf7f39b886a2df2a7a12c6 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -42,6 +42,17 @@ describe 'Edit Project Settings', feature: true do end end + context "When external issue tracker is enabled" do + it "does not hide issues tab" do + project.project_feature.update(issues_access_level: ProjectFeature::DISABLED) + allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new) + + visit namespace_project_path(project.namespace, project) + + expect(page).to have_selector(".shortcuts-issues") + end + end + context "pipelines subtabs" do it "shows builds when enabled" do visit namespace_project_pipelines_path(project.namespace, project) diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..32f33a3ca97e2f0c8376ba90f68fd3a7803bda6c --- /dev/null +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a Dockerfile file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile') + end + + scenario 'user can see Dockerfile dropdown' do + expect(page).to have_css('.dockerfile-selector') + end + + scenario 'user can pick a Dockerfile file from the dropdown', js: true do + find('.js-dockerfile-selector').click + wait_for_ajax + within '.dockerfile-selector' do + find('.dropdown-input-field').set('HTTPd') + find('.dropdown-content li', text: 'HTTPd').click + end + wait_for_ajax + + expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd') + expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/') + end +end diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb index 1921ea6d8aec39c9f0801c6568777ae5fed4a466..dd9622f16a0c4369dbd1d79bf8d56949913822d9 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do end it 'does not load on project#show' do - expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('') + expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({}) end it 'loads on new issue page' do visit new_namespace_project_issue_path(project.namespace, project) - expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('') + expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({}) end end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 1a71a03fbd9fc0f03736af9a7471c716ba9c666e..8b302a6aa23c7b0f776fd05c8e7e1f7cefd2e3ce 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do context 'setting an expiration date for a group link' do before do - visit namespace_project_group_links_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) select2 group.id, from: '#link_group_id' - fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') + fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') page.find('body').click click_on 'Share' end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index bfe59bdb90e75cb869cab6aa86cb9dc81ac5e225..7655c2b351f9e81d820d29814af1e2554eed1797 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 2f377312ea5aa94bc48599b6eaa9093ffc156ec3..6dae5c64b30edcfc9ce2d1da8770074244bf9260 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -27,7 +27,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template + assert_template save_changes end @@ -35,8 +35,7 @@ feature 'issuable templates', feature: true, js: true do select_template 'bug' wait_for_ajax select_option 'No template' - wait_for_ajax - preview_template('') + assert_template('') save_changes('') end @@ -44,9 +43,9 @@ feature 'issuable templates', feature: true, js: true do select_template 'bug' wait_for_ajax find_field('issue_description').send_keys(description_addition) - preview_template(template_content + description_addition) + assert_template(template_content + description_addition) select_option 'Reset template' - preview_template + assert_template save_changes end @@ -77,7 +76,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template("#{template_content}") + assert_template("#{template_content}") save_changes end end @@ -95,7 +94,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "feature-proposal" template' do select_template 'feature-proposal' wait_for_ajax - preview_template + assert_template save_changes end end @@ -122,17 +121,15 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects template' do select_template 'feature-proposal' wait_for_ajax - preview_template + assert_template save_changes end end end end - def preview_template(expected_content = template_content) - click_link 'Preview' - expect(page).to have_content expected_content - click_link 'Write' + def assert_template(expected_content = template_content) + expect(find('textarea')['value']).to eq(expected_content) end def save_changes(expected_content = template_content) diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb index c5e3d143d919ae4b25d7c6dd764fc6748ce4f68e..d82cf53c690fac4d50506eb0a69f717490b4d5ff 100644 --- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb +++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb @@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do end scenario "anonymous user visits the project's members page and sees the list of members" do - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) expect(current_path).to eq( - namespace_project_project_members_path(project.namespace, project)) + namespace_project_settings_members_path(project.namespace, project)) expect(page).to have_content(user.name) end end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb index 94995f7cf95ff5f7b406c3e0879514291553c7b0..cffb935ad5ad1db674c1107af0f4e6238ffd7dfb 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/group_links_spec.rb @@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t @group_link = create(:project_group_link, project: project, group: group) login_as(user) - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end it 'updates group access level' do @@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t wait_for_ajax - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) expect(first('.group_member')).to have_content('Guest') end diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index 7d0065ee2c4c6be39c8a22f5c1da5bcc24c40576..3385e5972ff7438442263ce56e08cc403ca4bf0e 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -19,7 +19,7 @@ feature 'Projects members', feature: true do context 'with a group invitee' do before do group_invitee - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'does not appear in the project members page' do @@ -33,7 +33,7 @@ feature 'Projects members', feature: true do before do group_invitee project_invitee - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'shows the project invitee, the project developer, and the group owner' do @@ -54,7 +54,7 @@ feature 'Projects members', feature: true do context 'with a group requester' do before do group.request_access(group_requester) - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'does not appear in the project members page' do @@ -68,7 +68,7 @@ feature 'Projects members', feature: true do before do group.request_access(group_requester) project.request_access(project_requester) - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'shows the project requester, the project developer, and the group owner' do diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 27a83fdcd1f6d7235e5b82d52524e2af2ff3379e..f136d9ce0fa20f60973c085a0699b4b3387d385d 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -14,17 +14,17 @@ feature 'Projects > Members > Master adds member with expiration date', feature: login_as(master) end - scenario 'expiration date is displayed in the members list' do + scenario 'expiration date is displayed in the members list', js: true do travel_to Time.zone.parse('2016-08-06 08:00') do - visit namespace_project_project_members_path(project.namespace, project) - + visit namespace_project_settings_members_path(project.namespace, project) page.within '.users-project-form' do select2(new_member.id, from: '#user_ids', multiple: true) fill_in 'expires_at', with: '2016-08-10' - click_on 'Add to project' end + find('.users-project-form').click + click_on 'Add to project' - page.within '.project_member:first-child' do + page.within "#project_member_#{new_member.project_members.first.id}" do expect(page).to have_content('Expires in 4 days') end end @@ -35,7 +35,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') visit namespace_project_project_members_path(project.namespace, project) - page.within '.project_member:first-child' do + page.within "#project_member_#{new_member.project_members.first.id}" do find('.js-access-expiration-date').set '2016-08-09' wait_for_ajax expect(page).to have_content('Expires in 3 days') diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d6ebb523f9533aa72292925f662fc7c874970ffa --- /dev/null +++ b/spec/features/projects/members/sorting_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +feature 'Projects > Members > Sorting', feature: true do + let(:master) { create(:user, name: 'John Doe') } + let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } + let(:project) { create(:empty_project) } + + background do + create(:project_member, :master, user: master, project: project, created_at: 5.days.ago) + create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) + + login_as(master) + end + + scenario 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') + end + + scenario 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') + end + + scenario 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') + end + + scenario 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') + end + + scenario 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') + end + + scenario 'sorts by recent sign in' do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') + end + + scenario 'sorts by oldest sign in' do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') + end + + def visit_members_list(sort:) + visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort) + end + + def first_member + page.all('ul.content-list > li').first.text + end + + def second_member + page.all('ul.content-list > li').last.text + end +end diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 97c42bd7f017f14b326ef81b77f5da14e23f858b..0b4dcaa39c693a264d1732569debc43e06d5d682 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do open_project_settings_menu click_link 'Members' - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) page.within('.content') do expect(page).not_to have_content(user.name) end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 3350a3aeefcc404013729c254b559c2bd7f1f82e..14e009daba831d66a867053eb42bd65cecbc6005 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Pipelines", feature: true, js: true do +describe 'Pipeline', :feature, :js do include GitlabRoutingHelper let(:project) { create(:empty_project) } @@ -19,7 +19,7 @@ describe "Pipelines", feature: true, js: true do @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build') @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') end @@ -38,6 +38,88 @@ describe "Pipelines", feature: true, js: true do expect(page).to have_css('#js-tab-pipeline.active') end + describe 'pipeline graph' do + context 'when pipeline has running builds' do + it 'shows a running icon and a cancel action for the running build' do + page.within('#ci-badge-deploy') do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_selector('.ci-action-icon-container .fa-ban') + expect(page).to have_content('deploy') + end + end + + it 'should be possible to cancel the running build' do + find('#ci-badge-deploy .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Cancel running') + end + end + + context 'when pipeline has successful builds' do + it 'shows the success icon and a retry action for the successful build' do + page.within('#ci-badge-build') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('build') + end + + page.within('#ci-badge-build .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'should be possible to retry the success build' do + find('#ci-badge-build .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') + end + end + + context 'when pipeline has failed builds' do + it 'shows the failed icon and a retry action for the failed build' do + page.within('#ci-badge-test') do + expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_content('test') + end + + page.within('#ci-badge-test .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'should be possible to retry the failed build' do + find('#ci-badge-test .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') + end + end + + context 'when pipeline has manual builds' do + it 'shows the skipped icon and a play action for the manual build' do + page.within('#ci-badge-manual-build') do + expect(page).to have_selector('.ci-status-icon-manual') + expect(page).to have_content('manual') + end + + page.within('#ci-badge-manual-build .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-play') + end + end + + it 'should be possible to play the manual build' do + find('#ci-badge-manual-build .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Play build') + end + end + + context 'when pipeline has external build' do + it 'shows the success icon and the generic comit status build' do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end + end + end + context 'page tabs' do it 'shows Pipeline and Builds tabs with link' do expect(page).to have_link('Pipeline') @@ -82,7 +164,7 @@ describe "Pipelines", feature: true, js: true do @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build') @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index f3731698a1832936ec7570c8d57e4e64820ae49b..ca18ac073d89a8d1d224861f96f192877c7ca537 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,211 +1,364 @@ require 'spec_helper' -describe "Pipelines" do - include GitlabRoutingHelper +describe 'Pipelines', :feature, :js do + include WaitForVueResource let(:project) { create(:empty_project) } - let(:user) { create(:user) } - before do - login_as(user) - project.team << [user, :developer] - end + context 'when user is logged in' do + let(:user) { create(:user) } - describe 'GET /:project/pipelines' do - let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') } + before do + login_as(user) + project.team << [user, :developer] + end - [:all, :running, :branches].each do |scope| - context "displaying #{scope}" do - let(:project) { create(:project) } + describe 'GET /:project/pipelines' do + let(:project) { create(:project) } + + let!(:pipeline) do + create( + :ci_empty_pipeline, + project: project, + ref: 'master', + status: 'running', + sha: project.commit.id, + ) + end - before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } + [:all, :running, :branches].each do |scope| + context "when displaying #{scope}" do + before do + visit_project_pipelines(scope: scope) + end - it { expect(page).to have_content(pipeline.short_sha) } + it 'contains pipeline commit short SHA' do + expect(page).to have_content(pipeline.short_sha) + end + end end - end - context 'anonymous access' do - before { visit namespace_project_pipelines_path(project.namespace, project) } + context 'when pipeline is cancelable' do + let!(:build) do + create(:ci_build, pipeline: pipeline, + stage: 'test', + commands: 'test') + end - it { expect(page).to have_http_status(:success) } - end + before do + build.run + visit_project_pipelines + end - context 'cancelable pipeline' do - let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + it 'indicates that pipeline can be canceled' do + expect(page).to have_link('Cancel') + expect(page).to have_selector('.ci-running') + end - before do - build.run - visit namespace_project_pipelines_path(project.namespace, project) + context 'when canceling' do + before { click_link('Cancel') } + + it 'indicated that pipelines was canceled' do + expect(page).not_to have_link('Cancel') + expect(page).to have_selector('.ci-canceled') + end + end end - it { expect(page).to have_link('Cancel') } - it { expect(page).to have_selector('.ci-running') } + context 'when pipeline is retryable' do + let!(:build) do + create(:ci_build, pipeline: pipeline, + stage: 'test', + commands: 'test') + end - context 'when canceling' do - before { click_link('Cancel') } + before do + build.drop + visit_project_pipelines + end - it { expect(page).not_to have_link('Cancel') } - it { expect(page).to have_selector('.ci-canceled') } - end - end + it 'indicates that pipeline can be retried' do + expect(page).to have_link('Retry') + expect(page).to have_selector('.ci-failed') + end - context 'retryable pipelines' do - let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + context 'when retrying' do + before { click_link('Retry') } - before do - build.drop - visit namespace_project_pipelines_path(project.namespace, project) + it 'shows running pipeline that is not retryable' do + expect(page).not_to have_link('Retry') + expect(page).to have_selector('.ci-running') + end + end end - it { expect(page).to have_link('Retry') } - it { expect(page).to have_selector('.ci-failed') } + context 'when pipeline has configuration errors' do + let(:pipeline) do + create(:ci_pipeline, :invalid, project: project) + end - context 'when retrying' do - before { click_link('Retry') } + before { visit_project_pipelines } - it { expect(page).not_to have_link('Retry') } - it { expect(page).to have_selector('.ci-running') } + it 'contains badge that indicates errors' do + expect(page).to have_content 'yaml invalid' + end + + it 'contains badge with tooltip which contains error' do + expect(pipeline).to have_yaml_errors + expect(page).to have_selector( + %Q{span[data-original-title="#{pipeline.yaml_errors}"]}) + end end - end - context 'with manual actions' do - let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } + context 'with manual actions' do + let!(:manual) do + create(:ci_build, :manual, + pipeline: pipeline, + name: 'manual build', + stage: 'test', + commands: 'test') + end - before { visit namespace_project_pipelines_path(project.namespace, project) } + before { visit_project_pipelines } - it { expect(page).to have_link('Manual build') } + it 'has a dropdown with play button' do + expect(page).to have_selector('.dropdown-toggle.btn.btn-default .icon-play') + end - context 'when playing' do - before { click_link('Manual build') } + it 'has link to the manual action' do + find('.js-pipeline-dropdown-manual-actions').click - it { expect(manual.reload).to be_pending } - end - end + expect(page).to have_link('manual build') + end - context 'for generic statuses' do - context 'when running' do - let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } + context 'when manual action was played' do + before do + find('.js-pipeline-dropdown-manual-actions').click + click_link('manual build') + end - before do - visit namespace_project_pipelines_path(project.namespace, project) + it 'enqueues manual action job' do + expect(manual.reload).to be_pending + end end + end - it 'is cancelable' do - expect(page).to have_link('Cancel') + context 'for generic statuses' do + context 'when running' do + let!(:running) do + create(:generic_commit_status, + status: 'running', + pipeline: pipeline, + stage: 'test') + end + + before { visit_project_pipelines } + + it 'is cancelable' do + expect(page).to have_link('Cancel') + end + + it 'has pipeline running' do + expect(page).to have_selector('.ci-running') + end + + context 'when canceling' do + before { click_link('Cancel') } + + it 'indicates that pipeline was canceled' do + expect(page).not_to have_link('Cancel') + expect(page).to have_selector('.ci-canceled') + end + end end - it 'has pipeline running' do - expect(page).to have_selector('.ci-running') + context 'when failed' do + let!(:status) do + create(:generic_commit_status, :pending, + pipeline: pipeline, + stage: 'test') + end + + before do + status.drop + visit_project_pipelines + end + + it 'is not retryable' do + expect(page).not_to have_link('Retry') + end + + it 'has failed pipeline' do + expect(page).to have_selector('.ci-failed') + end end + end - context 'when canceling' do - before { click_link('Cancel') } + context 'downloadable pipelines' do + context 'with artifacts' do + let!(:with_artifacts) do + create(:ci_build, :artifacts, :success, + pipeline: pipeline, + name: 'rspec tests', + stage: 'test') + end - it { expect(page).not_to have_link('Cancel') } - it { expect(page).to have_selector('.ci-canceled') } - end - end + before { visit_project_pipelines } - context 'when failed' do - let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') } + it 'has artifats' do + expect(page).to have_selector('.build-artifacts') + end - before do - status.drop - visit namespace_project_pipelines_path(project.namespace, project) - end + it 'has artifacts download dropdown' do + find('.js-pipeline-dropdown-download').click - it 'is not retryable' do - expect(page).not_to have_link('Retry') + expect(page).to have_link(with_artifacts.name) + end end - it 'has failed pipeline' do - expect(page).to have_selector('.ci-failed') + context 'with artifacts expired' do + let!(:with_artifacts_expired) do + create(:ci_build, :artifacts_expired, :success, + pipeline: pipeline, + name: 'rspec', + stage: 'test') + end + + before { visit_project_pipelines } + + it { expect(page).not_to have_selector('.build-artifacts') } end - end - end - context 'downloadable pipelines' do - context 'with artifacts' do - let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') } + context 'without artifacts' do + let!(:without_artifacts) do + create(:ci_build, :success, + pipeline: pipeline, + name: 'rspec', + stage: 'test') + end - before { visit namespace_project_pipelines_path(project.namespace, project) } + before { visit_project_pipelines } - it { expect(page).to have_selector('.build-artifacts') } - it { expect(page).to have_link(with_artifacts.name) } + it { expect(page).not_to have_selector('.build-artifacts') } + end end - context 'with artifacts expired' do - let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + context 'mini pipeline graph' do + let!(:build) do + create(:ci_build, :pending, pipeline: pipeline, + stage: 'build', + name: 'build') + end - before { visit namespace_project_pipelines_path(project.namespace, project) } + before { visit_project_pipelines } - it { expect(page).not_to have_selector('.build-artifacts') } - end + it 'should render a mini pipeline graph' do + expect(page).to have_selector('.js-mini-pipeline-graph') + expect(page).to have_selector('.js-builds-dropdown-button') + end - context 'without artifacts' do - let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + context 'when clicking a stage badge' do + it 'should open a dropdown' do + find('.js-builds-dropdown-button').trigger('click') - before { visit namespace_project_pipelines_path(project.namespace, project) } + expect(page).to have_link build.name + end - it { expect(page).not_to have_selector('.build-artifacts') } + it 'should be possible to cancel pending build' do + find('.js-builds-dropdown-button').trigger('click') + find('a.js-ci-action-icon').trigger('click') + + expect(page).to have_content('canceled') + expect(build.reload).to be_canceled + end + end end end - end - describe 'POST /:project/pipelines' do - let(:project) { create(:project) } + describe 'POST /:project/pipelines' do + let(:project) { create(:project) } + + before do + visit new_namespace_project_pipeline_path(project.namespace, project) + end + + context 'for valid commit' do + before { fill_in('pipeline[ref]', with: 'master') } - before { visit new_namespace_project_pipeline_path(project.namespace, project) } + context 'with gitlab-ci.yml' do + before { stub_ci_pipeline_to_return_yaml_file } - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } + it 'creates a new pipeline' do + expect { click_on 'Create pipeline' } + .to change { Ci::Pipeline.count }.by(1) + end + end - context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } + context 'without gitlab-ci.yml' do + before { click_on 'Create pipeline' } - it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } + it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + end end - context 'without gitlab-ci.yml' do - before { click_on 'Create pipeline' } + context 'for invalid commit' do + before do + fill_in('pipeline[ref]', with: 'invalid-reference') + click_on 'Create pipeline' + end - it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + it { expect(page).to have_content('Reference not found') } end end - context 'for invalid commit' do + describe 'Create pipelines' do + let(:project) { create(:project) } + before do - fill_in('pipeline[ref]', with: 'invalid-reference') - click_on 'Create pipeline' + visit new_namespace_project_pipeline_path(project.namespace, project) end - it { expect(page).to have_content('Reference not found') } + describe 'new pipeline page' do + it 'has field to add a new pipeline' do + expect(page).to have_field('pipeline[ref]') + expect(page).to have_content('Create for') + end + end + + describe 'find pipelines' do + it 'shows filtered pipelines', js: true do + fill_in('pipeline[ref]', with: 'fix') + find('input#ref').native.send_keys(:keydown) + + within('.ui-autocomplete') do + expect(page).to have_selector('li', text: 'fix') + end + end + end end end - describe 'Create pipelines', feature: true do - let(:project) { create(:project) } - + context 'when user is not logged in' do before do - visit new_namespace_project_pipeline_path(project.namespace, project) + visit namespace_project_pipelines_path(project.namespace, project) end - describe 'new pipeline page' do - it 'has field to add a new pipeline' do - expect(page).to have_field('pipeline[ref]') - expect(page).to have_content('Create for') - end + context 'when project is public' do + let(:project) { create(:project, :public) } + + it { expect(page).to have_content 'No pipelines to show' } + it { expect(page).to have_http_status(:success) } end - describe 'find pipelines' do - it 'shows filtered pipelines', js: true do - fill_in('pipeline[ref]', with: 'fix') - find('input#ref').native.send_keys(:keydown) + context 'when project is private' do + let(:project) { create(:project, :private) } - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') - end - end + it { expect(page).to have_content 'You need to sign in' } end end + + def visit_project_pipelines(**query) + visit namespace_project_pipelines_path(project.namespace, project, query) + wait_for_vue_resource + end end diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index bf60cca4ea4160e940bacf037c76afdca53db183..55d5d082c6e3174e1fe9f918b50775f777ec73c6 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -21,6 +21,16 @@ describe 'Edit Project Settings', feature: true do expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." expect(page).to have_button 'Save changes' end + + scenario 'shows a successful notice when the project is updated' do + visit edit_namespace_project_path(project.namespace, project) + + fill_in 'project_name_edit', with: 'hello world' + + click_button 'Save changes' + + expect(page).to have_content "Project 'hello world' was successfully updated." + end end describe 'Rename repository' do diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index f474e7e891bd949c60f5bd8a2bedbbf73c6d2b4d..86a07b2c679cef209b3bade257591a918581c5ef 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do include WaitForAjax let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:service) { project.create_mattermost_slash_commands_service } + let(:mattermost_enabled) { true } before do + Settings.mattermost['enabled'] = mattermost_enabled project.team << [user, :master] login_as(user) + visit edit_namespace_project_service_path(project.namespace, project, service) end - describe 'user visites the mattermost slash command config page', js: true do + describe 'user visits the mattermost slash command config page', js: true do it 'shows a help message' do - visit edit_namespace_project_service_path(project.namespace, project, service) - wait_for_ajax expect(page).to have_content("This service allows GitLab users to perform common") end - end - - describe 'saving a token' do - let(:token) { ('a'..'z').to_a.join } it 'shows the token after saving' do - visit edit_namespace_project_service_path(project.namespace, project, service) + token = ('a'..'z').to_a.join fill_in 'service_token', with: token click_on 'Save' @@ -35,14 +32,106 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to eq(token) end - end - describe 'the trigger url' do - it 'shows the correct url' do - visit edit_namespace_project_service_path(project.namespace, project, service) + it 'shows the add to mattermost button' do + expect(page).to have_link('Add to Mattermost') + end + + it 'shows an explanation if user is a member of no teams' do + stub_teams(count: 0) + + click_link 'Add to Mattermost' + + expect(page).to have_content('You aren’t a member of any team on the Mattermost instance') + expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team") + end + + it 'shows an explanation if user is a member of 1 team' do + stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(page).to have_content('The team where the slash commands will be used in') + expect(page).to have_content('This is the only available team.') + end + + it 'shows a disabled prefilled select if user is a member of 1 team' do + teams = stub_teams(count: 1) + + click_link 'Add to Mattermost' + + team_name = teams.first[1]['display_name'] + select_element = find('select#mattermost_team_id') + selected_option = select_element.find('option[selected]') + + expect(select_element['disabled']).to be(true) + expect(selected_option).to have_content(team_name.to_s) + end + + it 'has a hidden input for the prefilled value if user is a member of 1 team' do + teams = stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s) + end + + it 'shows an explanation user is a member of multiple teams' do + stub_teams(count: 2) + + click_link 'Add to Mattermost' + + expect(page).to have_content('Select the team where the slash commands will be used in') + expect(page).to have_content('The list shows all available teams.') + end + + it 'shows a select with team options user is a member of multiple teams' do + stub_teams(count: 2) + + click_link 'Add to Mattermost' + + select_element = find('select#mattermost_team_id') + selected_option = select_element.find('option[selected]') + + expect(select_element['disabled']).to be(false) + expect(selected_option).to have_content('Select team...') + # The 'Select team...' placeholder is item `0`. + expect(select_element.all('option').count).to eq(3) + end + + def stub_teams(count: 0) + teams = create_teams(count) + + allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams } + + teams + end + + def create_teams(count = 0) + teams = {} + + count.times do |i| + i += 1 + teams[i] = { id: i, display_name: i } + end + + teams + end + + describe 'mattermost service is not enabled' do + let(:mattermost_enabled) { false } + + it 'shows the correct trigger url' do + value = find_field('request_url').value + + expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + end + end + end - value = find_field('request_url').value - expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + describe 'stable logo url' do + it 'shows a publicly available logo' do + expect(File.exist?(Rails.root.join('public/slash-command-logo.png'))) end end end diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..32b32f7ae8eb0ca2414bf49db39c081e1df4e3c1 --- /dev/null +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Slack slash commands', feature: true do + include WaitForAjax + + given(:user) { create(:user) } + given(:project) { create(:project) } + given(:service) { project.create_slack_slash_commands_service } + + background do + project.team << [user, :master] + login_as(user) + end + + scenario 'user visits the slack slash command config page and shows a help message', js: true do + visit edit_namespace_project_service_path(project.namespace, project, service) + + wait_for_ajax + + expect(page).to have_content('This service allows GitLab users to perform common') + end + + scenario 'shows the token after saving' do + visit edit_namespace_project_service_path(project.namespace, project, service) + + fill_in 'service_token', with: 'token' + click_on 'Save' + + value = find_field('service_token').value + + expect(value).to eq('token') + end + + scenario 'shows the correct trigger url' do + visit edit_namespace_project_service_path(project.namespace, project, service) + + value = find_field('url').value + expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index caecd027aaa4bda68c48387b3f3a5aa1e437e295..a05b83959fb70379fcdd45095925dcf95750974e 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -169,16 +169,16 @@ describe "Search", feature: true do find('.dropdown-menu').click_link 'Issues assigned to me' sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") end it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("author:@#{user.username}") end it 'takes user to her MR page when MR assigned is clicked' do diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb index fe8cd7b7602380afaecdc79f1ec53123d9584134..e180ca53eb51e8f6c9e438a3a243456e0320573e 100644 --- a/spec/features/security/admin_access_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do include AccessMatchers describe "GET /admin/projects" do - subject { admin_namespaces_projects_path } + subject { admin_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 1897c8119d28b16c09880455dd81cfbc5b0af7c1..ecebabefff87cfb28f5a3bdff37ece893d6010f6 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end - describe "GET /:project_path/project_members" do - subject { namespace_project_project_members_path(project.namespace, project) } + describe "GET /:project_path/settings/members" do + subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index f52e23f94338c2365e0fd9c961054e9991bac2fc..9bc59a7c4f99c992d38008c9189544ce7dfb7d90 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end - describe "GET /:project_path/project_members" do - subject { namespace_project_project_members_path(project.namespace, project) } + describe "GET /:project_path/settings/members" do + subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index bed9e92fcb6071a60630a07550bf9200506f062a..a8d43b3d5816322a52a60db2abbcda63f1e43372 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for(:visitor) } end - describe "GET /:project_path/project_members" do - subject { namespace_project_project_members_path(project.namespace, project) } + describe "GET /:project_path/settings/members" do + subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 65544f79eba817d192cbe90d3ee8b1caa89d8e6b..9fde8d6e5cfb7582a0f6529ba31cd8c78853de88 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -10,10 +10,11 @@ feature 'Signup', feature: true do visit root_path - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: user.password + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_email_confirmation', with: user.email + fill_in 'new_user_password', with: user.password click_button "Register" expect(current_path).to eq users_almost_there_path @@ -29,10 +30,11 @@ feature 'Signup', feature: true do visit root_path - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: user.password + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_email_confirmation', with: user.email + fill_in 'new_user_password', with: user.password click_button "Register" expect(current_path).to eq dashboard_projects_path @@ -55,8 +57,9 @@ feature 'Signup', feature: true do click_button "Register" expect(current_path).to eq user_registration_path - expect(page).to have_content("error prohibited this user from being saved") + expect(page).to have_content("errors prohibited this user from being saved") expect(page).to have_content("Email has already been taken") + expect(page).to have_content("Email confirmation doesn't match") end it 'does not redisplay the password' do diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index cb95e7828dbf9cb9306b14e1970624be27f8b405..5470276bf062e5d38cb79efe0a7f24cc7be40cd2 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -17,4 +17,18 @@ feature 'Create Snippet', feature: true do expect(page).to have_content('My Snippet Title') expect(page).to have_content('Hello World!') end + + scenario 'Authenticated user creates a snippet with + in filename' do + fill_in 'personal_snippet_title', with: 'My Snippet Title' + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' + find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + end + + click_button 'Create snippet' + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('snippet+file+name') + expect(page).to have_content('Hello World!') + end end diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb index 08a97085a9c9e566befca9fdacc9196015fdf3d6..ca25c696f752d3672d7d19cb96ddb92b59a4e8d0 100644 --- a/spec/features/tags/master_creates_tag_spec.rb +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -34,7 +34,7 @@ feature 'Master creates tag', feature: true do expect(current_path).to eq( namespace_project_tag_path(project.namespace, project, 'v3.0')) expect(page).to have_content 'v3.0' - page.within 'pre.body' do + page.within 'pre.wrap' do expect(page).to have_content "Awesome tag message\n\n- hello\n- world" end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 88eabea7e3a8c74a6a151a6cfaef31ba77e8c3ae..4bda09276925728c82e8b67694be9b33020992ec 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -155,5 +155,24 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.todos-all-done', count: 1) end end + + context 'User has a Build Failed todo' do + let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } + + before do + login_as user + visit dashboard_todos_path + end + + it 'shows the todo' do + expect(page).to have_content 'The build failed for your merge request' + end + + it 'links to the pipelines for the merge request' do + href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target) + + expect(page).to have_link "merge request #{todo.target.to_reference}", href + end + end end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index be21b403084073543a5339f7ea4b904d38fb85e2..a8d00bb8e5aa2aab9538105fce6a100df77446a9 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -45,12 +45,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: it 'allows registering a new device with a name' do visit profile_account_path manage_two_factor_authentication - expect(page.body).to match("You've already enabled two-factor authentication using mobile") + expect(page).to have_content("You've already enabled two-factor authentication using mobile") u2f_device = register_u2f_device - expect(page.body).to match(u2f_device.name) - expect(page.body).to match('Your U2F device was registered') + expect(page).to have_content(u2f_device.name) + expect(page).to have_content('Your U2F device was registered') end it 'allows registering more than one device' do @@ -59,30 +59,30 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: # First device manage_two_factor_authentication first_device = register_u2f_device - expect(page.body).to match('Your U2F device was registered') + expect(page).to have_content('Your U2F device was registered') # Second device second_device = register_u2f_device - expect(page.body).to match('Your U2F device was registered') + expect(page).to have_content('Your U2F device was registered') - expect(page.body).to match(first_device.name) - expect(page.body).to match(second_device.name) + expect(page).to have_content(first_device.name) + expect(page).to have_content(second_device.name) expect(U2fRegistration.count).to eq(2) end it 'allows deleting a device' do visit profile_account_path manage_two_factor_authentication - expect(page.body).to match("You've already enabled two-factor authentication using mobile") + expect(page).to have_content("You've already enabled two-factor authentication using mobile") first_u2f_device = register_u2f_device second_u2f_device = register_u2f_device click_on "Delete", match: :first - expect(page.body).to match('Successfully deleted') + expect(page).to have_content('Successfully deleted') expect(page.body).not_to match(first_u2f_device.name) - expect(page.body).to match(second_u2f_device.name) + expect(page).to have_content(second_u2f_device.name) end end @@ -91,7 +91,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: visit profile_account_path manage_two_factor_authentication u2f_device = register_u2f_device - expect(page.body).to match('Your U2F device was registered') + expect(page).to have_content('Your U2F device was registered') logout # Second user @@ -100,7 +100,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: visit profile_account_path manage_two_factor_authentication register_u2f_device(u2f_device) - expect(page.body).to match('Your U2F device was registered') + expect(page).to have_content('Your U2F device was registered') expect(U2fRegistration.count).to eq(2) end @@ -117,8 +117,8 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: click_on 'Register U2F Device' expect(U2fRegistration.count).to eq(0) - expect(page.body).to match("The form contains the following error") - expect(page.body).to match("did not send a valid JSON response") + expect(page).to have_content("The form contains the following error") + expect(page).to have_content("did not send a valid JSON response") end it "allows retrying registration" do @@ -130,12 +130,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: click_on 'Setup New U2F Device' expect(page).to have_content('Your device was successfully set up') click_on 'Register U2F Device' - expect(page.body).to match("The form contains the following error") + expect(page).to have_content("The form contains the following error") # Successful registration register_u2f_device - expect(page.body).to match('Your U2F device was registered') + expect(page).to have_content('Your U2F device was registered') expect(U2fRegistration.count).to eq(1) end end @@ -160,10 +160,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: login_with(user) @u2f_device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') - click_on "Authenticate via U2F Device" - expect(page.body).to match('href="/users/sign_out"') + + expect(page).to have_content('We heard back from your U2F device') + expect(page).to have_css('.sign-out-link', visible: false) end end @@ -173,11 +172,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: login_with(user) @u2f_device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') - click_on "Authenticate via U2F Device" - expect(page.body).to match('href="/users/sign_out"') + expect(page).to have_content('We heard back from your U2F device') + expect(page).to have_css('.sign-out-link', visible: false) end end @@ -185,8 +182,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: login_with(user, remember: true) @u2f_device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') + expect(page).to have_content('We heard back from your U2F device') within 'div#js-authenticate-u2f' do field = first('input#user_remember_me', visible: false) @@ -208,11 +204,8 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: # Try authenticating user with the old U2F device login_as(current_user) @u2f_device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') - click_on "Authenticate via U2F Device" - - expect(page.body).to match('Authentication via U2F device failed') + expect(page).to have_content('We heard back from your U2F device') + expect(page).to have_content('Authentication via U2F device failed') end end @@ -229,11 +222,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: # Try authenticating user with the same U2F device login_as(current_user) @u2f_device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') - click_on "Authenticate via U2F Device" + expect(page).to have_content('We heard back from your U2F device') - expect(page.body).to match('href="/users/sign_out"') + expect(page).to have_css('.sign-out-link', visible: false) end end end @@ -243,11 +234,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name) login_as(user) unregistered_device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') - click_on "Authenticate via U2F Device" + expect(page).to have_content('We heard back from your U2F device') - expect(page.body).to match('Authentication via U2F device failed') + expect(page).to have_content('Authentication via U2F device failed') end end @@ -270,11 +259,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: [first_device, second_device].each do |device| login_as(user) device.respond_to_u2f_authentication - click_on "Sign in via U2F device" - expect(page.body).to match('We heard back from your U2F device') - click_on "Authenticate via U2F Device" + expect(page).to have_content('We heard back from your U2F device') - expect(page.body).to match('href="/users/sign_out"') + expect(page).to have_css('.sign-out-link', visible: false) logout end @@ -299,4 +286,50 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: end end end + + describe 'fallback code authentication' do + let(:user) { create(:user) } + + def assert_fallback_ui(page) + expect(page).to have_button('Verify code') + expect(page).to have_css('#user_otp_attempt') + expect(page).not_to have_link('Sign in via 2FA code') + expect(page).not_to have_css('#js-authenticate-u2f') + end + + before do + # Register and logout + login_as(user) + user.update_attribute(:otp_required_for_login, true) + visit profile_account_path + end + + describe 'when no u2f device is registered' do + before do + logout + login_with(user) + end + + it 'shows the fallback otp code UI' do + assert_fallback_ui(page) + end + end + + describe 'when a u2f device is registered' do + before do + manage_two_factor_authentication + @u2f_device = register_u2f_device + logout + login_with(user) + end + + it 'provides a button that shows the fallback otp code UI' do + expect(page).to have_link('Sign in via 2FA code') + + click_link('Sign in via 2FA code') + + assert_fallback_ui(page) + end + end + end end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index afa98f3f715c8c4612b47cc462150ba865058d8e..2de0fbe7ab2912aad41fd43804e0c45c1e2a349a 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -6,10 +6,11 @@ feature 'Users', feature: true, js: true do scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path click_link 'Register' - fill_in 'new_user_name', with: 'Name Surname' - fill_in 'new_user_username', with: 'Great' - fill_in 'new_user_email', with: 'name@mail.com' - fill_in 'new_user_password', with: 'password1234' + fill_in 'new_user_name', with: 'Name Surname' + fill_in 'new_user_username', with: 'Great' + fill_in 'new_user_email', with: 'name@mail.com' + fill_in 'new_user_email_confirmation', with: 'name@mail.com' + fill_in 'new_user_password', with: 'password1234' expect { click_button 'Register' }.to change { User.count }.by(1) end @@ -33,10 +34,11 @@ feature 'Users', feature: true, js: true do scenario 'Should show one error if email is already taken' do visit new_user_session_path click_link 'Register' - fill_in 'new_user_name', with: 'Another user name' - fill_in 'new_user_username', with: 'anotheruser' - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: '12341234' + fill_in 'new_user_name', with: 'Another user name' + fill_in 'new_user_username', with: 'anotheruser' + fill_in 'new_user_email', with: user.email + fill_in 'new_user_email_confirmation', with: user.email + fill_in 'new_user_password', with: '12341234' expect { click_button 'Register' }.to change { User.count }.by(0) expect(page).to have_text('Email has already been taken') expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 7f69e888f3249242c8ef22c8aa1d944dd7c5c364..97737d7ddc730a81d3263a6251a87dba5f3b8e70 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,24 +10,24 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } - let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - - before do - project1.team << [user, :master] - project2.team << [user, :developer] - project2.team << [user2, :developer] - - issue1 - issue2 - issue3 - end describe '#execute' do + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + before do + project1.team << [user, :master] + project2.team << [user, :developer] + project2.team << [user2, :developer] + + issue1 + issue2 + issue3 + end + context 'scope: all' do let(:scope) { 'all' } @@ -193,6 +193,15 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue2, issue3) end end + + it 'finds issues user can access due to group' do + group = create(:group) + project = create(:empty_project, group: group) + issue = create(:issue, project: project) + group.add_user(user, :owner) + + expect(issues).to include(issue) + end end context 'personal scope' do @@ -210,5 +219,43 @@ describe IssuesFinder do end end end + + context 'when project restricts issues' do + let(:scope) { nil } + + it "doesn't return team-only issues to non team members" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + issue = create(:issue, project: project) + + expect(issues).not_to include(issue) + end + + it "doesn't return issues if feature disabled" do + [project1, project2].each do |project| + project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) + end + + expect(issues.count).to eq 0 + end + end + end + + describe '.not_restricted_by_confidentiality' do + let(:authorized_user) { create(:user) } + let(:project) { create(:empty_project, namespace: authorized_user.namespace) } + let!(:public_issue) { create(:issue, project: project) } + let!(:confidential_issue) { create(:issue, project: project, confidential: true) } + + it 'returns non confidential issues for nil user' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) + end + + it 'returns non confidential issues for user not authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + end + + it 'returns all issues for user authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7c6860372cc2c1041e61b2f78049a90ac6e3c11b..4d21254323c78d104a5dbced40dd20f5c141e083 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -2,59 +2,203 @@ require 'spec_helper' describe NotesFinder do let(:user) { create :user } - let(:project) { create :project } - let(:note1) { create :note_on_commit, project: project } - let(:note2) { create :note_on_commit, project: project } - let(:commit) { note1.noteable } + let(:project) { create(:empty_project) } before do project.team << [user, :master] end describe '#execute' do - let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + it 'finds notes on snippets when project is public and user isnt a member' - before do - note1 - note2 + it 'finds notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'finds all notes' do - notes = NotesFinder.new.execute(project, user, params) - expect(notes.size).to eq(2) + it 'finds notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'raises an exception for an invalid target_type' do - params.merge!(target_type: 'invalid') - expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') + it "excludes notes on commits the author can't download" do + project = create(:project, :private) + note = create(:note_on_commit, project: project) + params = { target_type: 'commit', target_id: note.noteable.id } + + notes = described_class.new(project, create(:user), params).execute + + expect(notes.count).to eq(0) end - it 'filters out old notes' do - note2.update_attribute(:updated_at, 2.hours.ago) - notes = NotesFinder.new.execute(project, user, params) - expect(notes).to eq([note1]) + it 'succeeds when no notes found' do + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) end - context 'confidential issue notes' do - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + context 'on restricted projects' do + let(:project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, + snippets_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'publicly excludes notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on issues' do + create(:note_on_issue, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + end + + context 'for target' do + let(:project) { create(:project) } + let(:note1) { create :note_on_commit, project: project } + let(:note2) { create :note_on_commit, project: project } + let(:commit) { note1.noteable } + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + + before do + note1 + note2 + end + + it 'finds all notes' do + notes = described_class.new(project, user, params).execute + expect(notes.size).to eq(2) + end + + it 'finds notes on merge requests' do + note = create(:note_on_merge_request, project: project) + params = { target_type: 'merge_request', target_id: note.noteable.id } + + notes = described_class.new(project, user, params).execute + + expect(notes).to include(note) + end + + it 'finds notes on snippets' do + note = create(:note_on_project_snippet, project: project) + params = { target_type: 'snippet', target_id: note.noteable.id } - let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + notes = described_class.new(project, user, params).execute - it 'returns notes if user can see the issue' do - expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note]) + expect(notes.count).to eq(1) end - it 'raises an error if user can not see the issue' do + it 'raises an exception for an invalid target_type' do + params.merge!(target_type: 'invalid') + expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') + end + + it 'filters out old notes' do + note2.update_attribute(:updated_at, 2.hours.ago) + notes = described_class.new(project, user, params).execute + expect(notes).to eq([note1]) + end + + context 'confidential issue notes' do + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + + let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + + it 'returns notes if user can see the issue' do + expect(described_class.new(project, user, params).execute).to eq([confidential_note]) + end + + it 'raises an error if user can not see the issue' do + user = create(:user) + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error for project members with guest role' do + user = create(:user) + project.team << [user, :guest] + + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end + + describe '.search' do + let(:project) { create(:empty_project, :public) } + let(:note) { create(:note_on_issue, note: 'WoW', project: project) } + + it 'returns notes with matching content' do + expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note]) + end + + it 'returns commit notes user can access' do + note = create(:note_on_commit, project: project) + + expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note]) + end + + context "confidential issues" do + let(:user) { create(:user) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } + + it "returns notes with matching content if user can see the issue" do + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note]) + end + + it "does not return notes with matching content if user can not see the issue" do user = create(:user) - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty end - it 'raises an error for project members with guest role' do + it "does not return notes with matching content for project members with guest role" do user = create(:user) project.team << [user, :guest] + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty + end + + it "does not return notes with matching content for unauthenticated users" do + expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty + end + end + + context 'inlines SQL filters on subqueries for performance' do + let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql } + let(:number_of_noteable_types) { 4 } + + specify 'project_id check' do + expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2) + end - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + specify 'search filter' do + expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types end end end diff --git a/spec/fixtures/config/redis_config_with_env.yml b/spec/fixtures/config/redis_config_with_env.yml new file mode 100644 index 0000000000000000000000000000000000000000..f5860f37e47547c4b751c8d9cea9fb5889e2b859 --- /dev/null +++ b/spec/fixtures/config/redis_config_with_env.yml @@ -0,0 +1,2 @@ +test: + url: <%= ENV['TEST_GITLAB_REDIS_URL'] %> diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 15863d444f8d5a5f6f6aa2d2758d71b298017e30..92053e5a7c64dcd4a5da7df935ec65c06802a566 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ApplicationHelper do + include UploadHelpers + describe 'current_controller?' do it 'returns true when controller matches argument' do stub_controller_name('foo') @@ -52,10 +54,8 @@ describe ApplicationHelper do end describe 'project_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - project = create(:project, avatar: File.open(avatar_file_path)) + project = create(:project, avatar: File.open(uploaded_image_temp_path)) avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). @@ -74,10 +74,8 @@ describe ApplicationHelper do end describe 'avatar_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -88,7 +86,7 @@ describe ApplicationHelper do # Must be stubbed after the stub above, and separately stub_config_setting(url: Settings.send(:build_gitlab_url)) - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -102,7 +100,7 @@ describe ApplicationHelper do describe 'using a User' do it 'returns an URL for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 1d494edcd3b6d6e9ae6a9127d747c8f62961e891..810311e2b1a2ff2d06ebbc5b49dd9be1ee05c4d6 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -170,4 +170,14 @@ describe GitlabMarkdownHelper do expect(doc.content).to eq "@#{user.username}, can you look at this?..." end end + + describe '#cross_project_reference' do + it 'shows the full MR reference' do + expect(helper.cross_project_reference(project, merge_request)).to include(project.path_with_namespace) + end + + it 'shows the full issue reference' do + expect(helper.cross_project_reference(project, issue)).to include(project.path_with_namespace) + end + end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 233d00534e507446288b4c577f43e5b6be49c1ba..c8b0d86425fa706731191fc9a05e72f1d06ff502 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -6,7 +6,7 @@ describe GroupsHelper do it 'returns an url for the avatar' do group = create(:group) - group.avatar = File.open(avatar_file_path) + group.avatar = fixture_file_upload(avatar_file_path) group.save! expect(group_icon(group.path).to_s). to match("/uploads/group/avatar/#{group.id}/banana_sample.gif") diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 187b891b9273c2b85f121af71e9866bcfe33c882..10f293cddf599cf6fe5d2faf122e52cc776617ef 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -25,24 +25,37 @@ describe ImportHelper do end end - describe '#github_project_link' do - context 'when provider does not specify a custom URL' do - it 'uses default GitHub URL' do - allow(Gitlab.config.omniauth).to receive(:providers). + describe '#provider_project_link' do + context 'when provider is "github"' do + context 'when provider does not specify a custom URL' do + it 'uses default GitHub URL' do + allow(Gitlab.config.omniauth).to receive(:providers). and_return([Settingslogic.new('name' => 'github')]) - expect(helper.github_project_link('octocat/Hello-World')). + expect(helper.provider_project_link('github', 'octocat/Hello-World')). to include('href="https://github.com/octocat/Hello-World"') + end end - end - context 'when provider specify a custom URL' do - it 'uses custom URL' do - allow(Gitlab.config.omniauth).to receive(:providers). + context 'when provider specify a custom URL' do + it 'uses custom URL' do + allow(Gitlab.config.omniauth).to receive(:providers). and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) - expect(helper.github_project_link('octocat/Hello-World')). + expect(helper.provider_project_link('github', 'octocat/Hello-World')). to include('href="https://github.company.com/octocat/Hello-World"') + end + end + end + + context 'when provider is "gitea"' do + before do + assign(:gitea_host_url, 'https://try.gitea.io/') + end + + it 'uses given host' do + expect(helper.provider_project_link('gitea', 'octocat/Hello-World')). + to include('href="https://try.gitea.io/octocat/Hello-World"') end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index abe08d95eced9bcf29aa172cef6b7ae0916efc72..9c7e0ee2fe02d4b866059c9997c448b49ae8669f 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -98,15 +98,15 @@ describe IssuesHelper do end end - describe '#award_active_class' do + describe '#award_state_class' do let!(:upvote) { create(:award_emoji) } - it "returns empty string for unauthenticated user" do - expect(award_active_class(AwardEmoji.all, nil)).to eq("") + it "returns disabled string for unauthenticated user" do + expect(award_state_class(AwardEmoji.all, nil)).to eq("disabled") end it "returns active string for author" do - expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active") + expect(award_state_class(AwardEmoji.all, upvote.user)).to eq("active") end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 903224589ddae06ec39859337eb3fa123fc09175..1f221487393fbf94ff215643593116d1a27069ea 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -62,4 +62,19 @@ describe MergeRequestsHelper do it { is_expected.to eq([source_title, target_title]) } end end + + describe 'mr_widget_refresh_url' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:project) { create(:project) } + + it 'returns correct url for MR' do + expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh" + + expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url) + end + + it 'returns empty string for nil' do + expect(mr_widget_refresh_url(nil)).to end_with('') + end + end end diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4627a1e1872668693adb35d2a96cc4c9a40a90e1 --- /dev/null +++ b/spec/helpers/storage_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe StorageHelper do + describe '#storage_counter' do + it 'formats bytes to one decimal place' do + expect(helper.storage_counter(1.23.megabytes)).to eq '1.2 MB' + end + + it 'does not add decimals for sizes < 1 MB' do + expect(helper.storage_counter(23.5.kilobytes)).to eq '24 KB' + end + + it 'does not add decimals for zeroes' do + expect(helper.storage_counter(2.megabytes)).to eq '2 MB' + end + + it 'uses commas as thousands separator' do + expect(helper.storage_counter(100_000_000_000_000_000)).to eq '90,949.5 TB' + end + end +end diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index 837b0de9a4cba692e5127759940706926251200c..ad7f032d1e5fb90a8a1d545aad2fb56034c4d559 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' require_relative '../../config/initializers/secret_token' describe 'create_tokens', lib: true do + include StubENV + let(:secrets) { ActiveSupport::OrderedOptions.new } before do - allow(ENV).to receive(:[]).and_call_original allow(File).to receive(:write) allow(File).to receive(:delete) allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets) @@ -17,7 +18,7 @@ describe 'create_tokens', lib: true do context 'setting secret_key_base and otp_key_base' do context 'when none of the secrets exist' do before do - allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil) + stub_env('SECRET_KEY_BASE', nil) allow(File).to receive(:exist?).with('.secret').and_return(false) allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false) allow(self).to receive(:warn_missing_secret) @@ -69,7 +70,7 @@ describe 'create_tokens', lib: true do context 'when secret_key_base exists in the environment and secrets.yml' do before do - allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key') + stub_env('SECRET_KEY_BASE', 'env_key') secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' end diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 7792acffac285691fe088a9b5d5b51d430868ed0..dcbcd014dc3c2730c05f4e411268e0872d18ee12 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -1,15 +1,28 @@ { - "plugins": ["jasmine"], "env": { "jasmine": true }, "extends": "plugin:jasmine/recommended", + "globals": { + "appendLoadFixtures": false, + "appendLoadStyleFixtures": false, + "appendSetFixtures": false, + "appendSetStyleFixtures": false, + "getJSONFixture": false, + "loadFixtures": false, + "loadJSONFixtures": false, + "loadStyleFixtures": false, + "preloadFixtures": false, + "preloadStyleFixtures": false, + "readFixtures": false, + "sandbox": false, + "setFixtures": false, + "setStyleFixtures": false, + "spyOnEvent": false + }, + "plugins": ["jasmine"], "rules": { "prefer-arrow-callback": 0, "func-names": 0 - }, - "globals": { - "fixture": false, - "spyOnEvent": false } } diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index a3171353bfb5817e71a03b1e943abe31f8c6da47..cf19aa050315a0b1e56983d5bad83673fae5a811 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,42 +1,44 @@ -/* eslint-disable */ +/*= require lib/utils/text_utility */ /*= require abuse_reports */ -/*= require jquery */ - ((global) => { - const FIXTURE = 'abuse_reports.html'; - const MAX_MESSAGE_LENGTH = 500; + describe('Abuse Reports', () => { + const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; + const MAX_MESSAGE_LENGTH = 500; + + let messages; - function assertMaxLength($message) { - expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); - } + const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); + const findMessage = searchText => messages.filter( + (index, element) => element.innerText.indexOf(searchText) > -1, + ).first(); - describe('Abuse Reports', function() { - fixture.preload(FIXTURE); + preloadFixtures(FIXTURE); - beforeEach(function() { - fixture.load(FIXTURE); - new global.AbuseReports(); + beforeEach(function () { + loadFixtures(FIXTURE); + this.abuseReports = new global.AbuseReports(); + messages = $('.abuse-reports .message'); }); - it('should truncate long messages', function() { - const $longMessage = $('#long'); + + it('should truncate long messages', () => { + const $longMessage = findMessage('LONG MESSAGE'); expect($longMessage.data('original-message')).toEqual(jasmine.anything()); assertMaxLength($longMessage); }); - it('should not truncate short messages', function() { - const $shortMessage = $('#short'); + it('should not truncate short messages', () => { + const $shortMessage = findMessage('SHORT MESSAGE'); expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); }); - it('should allow clicking a truncated message to expand and collapse the full message', function() { - const $longMessage = $('#long'); + it('should allow clicking a truncated message to expand and collapse the full message', () => { + const $longMessage = findMessage('LONG MESSAGE'); $longMessage.click(); expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); $longMessage.click(); assertMaxLength($longMessage); }); }); - })(window.gl); diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 8640cd440850be2d4d23095b4b0cdf174515a65a..b3617a45bd47207ffb67ea5acc3f552edee82e50 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ + /*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ /*= require pager */ @@ -6,7 +7,7 @@ (() => { window.gon || (window.gon = {}); - const fixtureTemplate = 'event_filter.html'; + const fixtureTemplate = 'static/event_filter.html.raw'; const filters = [ { id: 'all', @@ -34,7 +35,7 @@ describe('Activities', () => { beforeEach(() => { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); new gl.Activities(); }); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 7b2e3db6218a4a12079475526ffaa5cfd0d0ebc3..faba2837d419c07df28ccc5f6928bf9eda15ca3c 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* global AwardsHandler */ /*= require awards_handler */ /*= require jquery */ @@ -33,9 +34,9 @@ }; describe('AwardsHandler', function() { - fixture.preload('issues/open-issue.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); beforeEach(function() { - fixture.load('issues/open-issue.html.raw'); + loadFixtures('issues/open-issue.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { return function(url, emoji, cb) { diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index b4573e53a4e01ec73acf6c8085acb68102c74e3d..e77d732a32a7a67d3246d191ab59cb9a150f3a92 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -6,7 +6,7 @@ describe('Autosize behavior', function() { var load; beforeEach(function() { - return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>'); }); it('does not overwrite the resize property', function() { load(); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index efb1203eb2f74069a5d03e4adad24d1fff134203..1a1f34cfdc08ee7b5162897e635c83d9379a7e0c 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,13 +1,13 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ /*= require behaviors/quick_submit */ (function() { describe('Quick Submit behavior', function() { var keydownEvent; - fixture.preload('behaviors/quick_submit.html'); + preloadFixtures('static/behaviors/quick_submit.html.raw'); beforeEach(function() { - fixture.load('behaviors/quick_submit.html'); + loadFixtures('static/behaviors/quick_submit.html.raw'); $('form').submit(function(e) { // Prevent a form submit from moving us off the testing page return e.preventDefault(); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index c3f4c867d6a346b0bc8c625fa126a864f18fdc80..1f62591c06d6e3f599726dc181ceef1dcaa15122 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -4,9 +4,9 @@ (function() { describe('requiresInput', function() { - fixture.preload('behaviors/requires_input.html'); + preloadFixtures('static/behaviors/requires_input.html.raw'); beforeEach(function() { - return fixture.load('behaviors/requires_input.html'); + return loadFixtures('static/behaviors/requires_input.html.raw'); }); it('disables submit when any field is required', function() { $('.js-requires-input').requiresInput(); diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index b84dfc8197beb764d80ee799c20570466f1b9cd0..b3a1afa28a564906fc2e6d06be0bb66a6b5b7231 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,4 +1,11 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */ +/* global Vue */ +/* global BoardService */ +/* global boardsMockInterceptor */ +/* global Cookies */ +/* global listObj */ +/* global listObjDuplicate */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 90cb89265451cd45f35a6ef0e88c77ace7e20074..c8a61a0a9b5dbcb531b50743373361fab7c89d26 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global BoardService */ +/* global ListIssue */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index dfbcbe3a7c1db031e9ce1fda89d700ac050167e1..7d942ec3d658e1b2a8d5800fb035e2f09de5a0cf 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global Vue */ +/* global boardsMockInterceptor */ +/* global BoardService */ +/* global List */ +/* global listObj */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index fcb3d8f17d8c6326ce8e216b0b2db4d478f36956..8d3e2237fda5a6ddb7c5cd4e3a5300461d4e9394 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, quote-props */ + const listObj = { id: 1, position: 0, diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index 133712debab56a2e025297bf87a377188e214e39..ea953d0f5a556b9aa5c76da61e868406196acc9a 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -2,10 +2,10 @@ (() => { describe('Linked Tabs', () => { - fixture.preload('linked_tabs'); + preloadFixtures('static/linked_tabs.html.raw'); beforeEach(() => { - fixture.load('linked_tabs'); + loadFixtures('static/linked_tabs.html.raw'); }); describe('when is initialized', () => { diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 3983cad4c137122cdaff5bd93b4aed2cbcfccbf5..0c556382980bb13b4307493b36a79be3003b0a6c 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -17,10 +17,10 @@ describe('Build', () => { offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, })); - fixture.preload('builds/build-with-artifacts.html.raw'); + preloadFixtures('builds/build-with-artifacts.html.raw'); beforeEach(() => { - fixture.load('builds/build-with-artifacts.html.raw'); + loadFixtures('builds/build-with-artifacts.html.raw'); spyOn($, 'ajax'); }); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 93f73fa0e9a669a5a69525f954888bd7a2b3abdd..3f6b328348de46fb1ee3eff2407b6497448de1e7 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, padded-blocks */ + /*= require sidebar */ /*= require jquery */ /*= require js.cookie */ @@ -6,7 +7,7 @@ ((global) => { describe('Dashboard', () => { - const fixtureTemplate = 'dashboard.html'; + const fixtureTemplate = 'static/dashboard.html.raw'; function todosCountText() { return $('.js-todos-count').text(); @@ -16,9 +17,9 @@ $(document).trigger('todo:toggle', newCount); } - fixture.preload(fixtureTemplate); + preloadFixtures(fixtureTemplate); beforeEach(() => { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); new global.Sidebar(); }); diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 9fdbab3a9e983d1d47e25de61e00e723c7a5ed7b..8ece24555c5f86e90b8ab87b5299f59ce0e9beb0 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,5 +1,5 @@ -/* eslint-disable */ //= require lib/utils/datetime_utility + (() => { describe('Date time utils', () => { describe('get day name', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 9b2845af608835168b08f339011546690d17b6fd..18805d26ac0229342853c0ebe40313735a3406ed 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,8 +1,11 @@ -/* eslint-disable */ +/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ +/* global CommentsStore */ + //= require vue //= require diff_notes/models/discussion //= require diff_notes/models/note //= require diff_notes/stores/comments + (() => { function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create('a', noteId, true, resolved, 'test'); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 4bae3f30bb5b00ad85e6d618f5d8e12594409a17..056e4d41e93e56b6a8067fb601c39646bd445179 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -2,10 +2,10 @@ //= require environments/components/environment_actions describe('Actions Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); }); it('should render a dropdown with the provided actions', () => { diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 9f82567c35b70a2ce7f6530656169ccc290e5cc6..950a5d53fad76021569336326ffe4fa601552ab8 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -2,9 +2,9 @@ //= require environments/components/environment_external_url describe('External URL Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); }); it('should link to the provided externalUrl prop', () => { diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 5d7c6b2411d050b242adc09060ab87fdb2400292..c178b9cc1ec17c4e33449e4b485d25f89cc551b8 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -3,9 +3,9 @@ //= require environments/components/environment_item describe('Environment item', () => { - fixture.preload('environments/table.html'); + preloadFixtures('static/environments/table.html.raw'); beforeEach(() => { - fixture.load('environments/table.html'); + loadFixtures('static/environments/table.html.raw'); }); describe('When item is folder', () => { diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 77ba0ab38eca1632ac167ebb0235f54f0701c0b0..21241116e29f86cbb9d85c9bd7d60a7d0e29db1a 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,12 +1,12 @@ //= require vue //= require environments/components/environment_rollback describe('Rollback Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); const retryURL = 'https://gitlab.com/retry'; beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); }); it('Should link to the provided retryUrl', () => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 84a41b2bf4678b9ca84b950b109c10fcecf1e916..bb998a32f32ff31f8d2d1ca3a4edf41a9d150b70 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,13 +1,13 @@ //= require vue //= require environments/components/environment_stop describe('Stop Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); let stopURL; let component; beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); stopURL = '/stop'; component = new window.gl.environmentsList.StopComponent({ diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 76309930f27861decf6fd0ca4d5fa8d6fb00dcf1..91846bb9143bd7074f35f9db0e22f8dc93a4f696 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -6,7 +6,7 @@ describe('jQuery extensions', function() { describe('disable', function() { beforeEach(function() { - return fixture.set('<input type="text" />'); + return setFixtures('<input type="text" />'); }); it('adds the disabled attribute', function() { var $input; @@ -23,7 +23,7 @@ }); return describe('enable', function() { beforeEach(function() { - return fixture.set('<input type="text" disabled="disabled" class="disabled" />'); + return setFixtures('<input type="text" disabled="disabled" class="disabled" />'); }); it('removes the disabled attribute', function() { var $input; diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..3b71c255b309b8134079e293227966a0cd5b7f6b --- /dev/null +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -0,0 +1,25 @@ +/*= require extensions/object */ + +describe('Object extensions', () => { + describe('assign', () => { + it('merges source object into target object', () => { + const targetObj = {}; + const sourceObj = { + foo: 'bar', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('bar'); + }); + + it('merges object with the same properties', () => { + const targetObj = { + foo: 'bar', + }; + const sourceObj = { + foo: 'baz', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('baz'); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..ce61b73aa8a149121506d0f6e64ebd47179d859a --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -0,0 +1,107 @@ +//= require extensions/array +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Dropdown Utils', () => { + describe('getEscapedText', () => { + it('should return same word when it has no space', () => { + const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + expect(escaped).toBe('textWithoutSpace'); + }); + + it('should escape with double quotes', () => { + let escaped = gl.DropdownUtils.getEscapedText('text with space'); + expect(escaped).toBe('"text with space"'); + + escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + expect(escaped).toBe('"won\'t fix"'); + }); + + it('should escape with single quotes', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + expect(escaped).toBe('\'won"t fix\''); + }); + + it('should escape with single quotes by default', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + expect(escaped).toBe('\'won"t\' fix\''); + }); + }); + + describe('filterWithSymbol', () => { + const item = { + title: '@root', + }; + + it('should filter without symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with colon', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('filterHint', () => { + it('should filter', () => { + let updatedItem = gl.DropdownUtils.filterHint({ + hint: 'label', + }, 'l'); + expect(updatedItem.droplab_hidden).toBe(false); + + updatedItem = gl.DropdownUtils.filterHint({ + hint: 'label', + }, 'o'); + expect(updatedItem.droplab_hidden).toBe(true); + }); + + it('should return droplab_hidden false when item has no hint', () => { + const updatedItem = gl.DropdownUtils.filterHint({}, ''); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('setDataValueIfSelected', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + .and.callFake(() => {}); + }); + + it('calls addWordToInput when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + }); + + it('returns true when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(true); + }); + + it('returns false when dataValue does not exist', () => { + const selected = { + getAttribute: () => null, + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(false); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..d0d27ceb4a6f8b012da929d357e8b5e4ab5410b3 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -0,0 +1,59 @@ +//= require extensions/array +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Filtered Search Dropdown Manager', () => { + describe('addWordToInput', () => { + function getInputValue() { + return document.querySelector('.filtered-search').value; + } + + function setInputValue(value) { + document.querySelector('.filtered-search').value = value; + } + + beforeEach(() => { + const input = document.createElement('input'); + input.classList.add('filtered-search'); + document.body.appendChild(input); + }); + + afterEach(() => { + document.querySelector('.filtered-search').outerHTML = ''; + }); + + describe('input has no existing value', () => { + it('should add just tokenName', () => { + gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + expect(getInputValue()).toBe('milestone:'); + }); + + it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + expect(getInputValue()).toBe('label:none'); + }); + }); + + describe('input has existing value', () => { + it('should be able to just add tokenName', () => { + setInputValue('a'); + gl.FilteredSearchDropdownManager.addWordToInput('author'); + expect(getInputValue()).toBe('author:'); + }); + + it('should replace tokenValue', () => { + setInputValue('author:roo'); + gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); + expect(getInputValue()).toBe('author:@root'); + }); + + it('should add tokenValues containing spaces', () => { + setInputValue('label:~"test'); + gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); + expect(getInputValue()).toBe('label:~\'"test me"\''); + }); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..6df7c0e44efe9e29c5c8d4249ccbbacc2ac1e977 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -0,0 +1,104 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys + +(() => { + describe('Filtered Search Token Keys', () => { + describe('get', () => { + let tokenKeys; + + beforeEach(() => { + tokenKeys = gl.FilteredSearchTokenKeys.get(); + }); + + it('should return tokenKeys', () => { + expect(tokenKeys !== null).toBe(true); + }); + + it('should return tokenKeys as an array', () => { + expect(tokenKeys instanceof Array).toBe(true); + }); + }); + + describe('getConditions', () => { + let conditions; + + beforeEach(() => { + conditions = gl.FilteredSearchTokenKeys.getConditions(); + }); + + it('should return conditions', () => { + expect(conditions !== null).toBe(true); + }); + + it('should return conditions as an array', () => { + expect(conditions instanceof Array).toBe(true); + }); + }); + + describe('searchByKey', () => { + it('should return null when key not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchBySymbol', () => { + it('should return null when symbol not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by symbol', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByKeyParam', () => { + it('should return null when key param not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByConditionUrl', () => { + it('should return null when condition url not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by url', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + expect(result).toBe(conditions[0]); + }); + }); + + describe('searchByConditionKeyValue', () => { + it('should return null when condition tokenKey and value not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by tokenKey and value', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); + expect(result).toEqual(conditions[0]); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..ac7f8e9cbcd59efe00089d6f9df0f723045f7174 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -0,0 +1,104 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys +//= require filtered_search/filtered_search_tokenizer + +(() => { + describe('Filtered Search Tokenizer', () => { + describe('processTokens', () => { + it('returns for input containing only search value', () => { + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(0); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing only tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + expect(results.searchToken).toBe(''); + expect(results.tokens.length).toBe(4); + expect(results.tokens[3]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Very Important"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('v1.0'); + expect(results.tokens[2].symbol).toBe('%'); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].symbol).toBe(''); + }); + + it('returns for input starting with search value and ending with tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0]).toBe(results.lastToken); + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].symbol).toBe(''); + }); + + it('returns for input starting with tokens and ending with search value', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('user'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing search value wrapped between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Won\'t fix"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].symbol).toBe(''); + }); + + it('returns for input containing search value in between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].symbol).toBe(''); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('Doing'); + expect(results.tokens[2].symbol).toBe('~'); + }); + }); + }); +})(); diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml deleted file mode 100644 index 2ec302abcb7dc9e58bd4f0b03e993c1582663e34..0000000000000000000000000000000000000000 --- a/spec/javascripts/fixtures/abuse_reports.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.abuse-reports - .message#long - Cat ipsum dolor sit amet, hide head under blanket so no one can see. - Gate keepers of hell eat and than sleep on your face but hunt by meowing - loudly at 5am next to human slave food dispenser cats go for world - domination or chase laser, yet poop on grasses chirp at birds. Cat is love, - cat is life chase after silly colored fish toys around the house climb a - tree, wait for a fireman jump to fireman then scratch his face fall asleep - on the washing machine lies down always hungry so caticus cuteicus. Sit on - human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to - pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under - blanket so no one can see throwup on your pillow. - .message#short - Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your - beauty sleep 18 hours - checked, be fabulous for the rest of the day - - checked! for shake treat bag. diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb new file mode 100644 index 0000000000000000000000000000000000000000..de673f94d72c4509126ddc3e18e79d4be9a46a67 --- /dev/null +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let!(:abuse_report) { create(:abuse_report) } + let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') } + let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) } + + render_views + + before(:all) do + clean_frontend_fixtures('abuse_reports/') + end + + before(:each) do + sign_in(admin) + end + + it 'abuse_reports/abuse_reports_list.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..ae745b292e6e8eee35ca50d7e394ed1e3bb4be16 --- /dev/null +++ b/spec/javascripts/fixtures/issuable_filter.html.haml @@ -0,0 +1,8 @@ +%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} + %input{id: 'utf8', name: 'utf8', value: '✓'} + %input{id: 'check_all_issues', name: 'check_all_issues'} + %input{id: 'search', name: 'search'} + %input{id: 'author_id', name: 'author_id'} + %input{id: 'assignee_id', name: 'assignee_id'} + %input{id: 'milestone_title', name: 'milestone_title'} + %input{id: 'label_name', name: 'label_name'} diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..e9bf7568e95f9d0f037ad990aac55cc955a0c370 --- /dev/null +++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml @@ -0,0 +1,8 @@ +%div.js-builds-dropdown-tests + %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'} + Dropdown + %div.js-builds-dropdown-container + %div.js-builds-dropdown-list + + %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml index deca50ceaa74b54c1c90755501144b587e23c416..c0b5ab4411eef3f72fbfdf6a10fa1409369d1f75 100644 --- a/spec/javascripts/fixtures/pipeline_graph.html.haml +++ b/spec/javascripts/fixtures/pipeline_graph.html.haml @@ -8,8 +8,7 @@ %ul %li.build .curve - .build-content - %a - %svg - .ci-status-text - stop_review + %a + %svg + .ci-status-text + stop_review diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml index 4547feeb212ffe3aa9c7ff8b8ad0c6d64f03cc53..9d1f78771167c6f40febaa9a1fd2005a2450e334 100644 --- a/spec/javascripts/fixtures/project_title.html.haml +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -4,7 +4,7 @@ GitLab Org %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} GitLab Test - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content" } + %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" } .js-dropdown-menu-projects .dropdown-menu.dropdown-select.dropdown-menu-projects .dropdown-title diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 4919d77e5a46fa1a2b90b0753777694f8fe4cb65..4ce7f5c601aab978c8c2fe0ee1c0b686a9a40966 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -1 +1,445 @@ -[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}] +[{ + "id": 9, + "description": "", + "default_branch": null, + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:root/test.git", + "http_url_to_repo": "http://localhost:3000/root/test.git", + "web_url": "http://localhost:3000/root/test", + "owner": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/root" + }, + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-14T19:08:05.364Z", + "last_activity_at": "2016-01-14T19:08:07.418Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 1, + "name": "root", + "path": "root", + "owner_id": 1, + "created_at": "2016-01-13T20:19:44.439Z", + "updated_at": "2016-01-13T20:19:44.439Z", + "description": "", + "avatar": null + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 0, + "permissions": { + "project_access": null, + "group_access": null + } +}, { + "id": 8, + "description": "Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:h5bp/html5-boilerplate.git", + "http_url_to_repo": "http://localhost:3000/h5bp/html5-boilerplate.git", + "web_url": "http://localhost:3000/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "H5bp / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:57.525Z", + "last_activity_at": "2016-01-13T20:27:57.280Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "H5bp", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-01-13T20:19:57.239Z", + "updated_at": "2016-01-13T20:19:57.239Z", + "description": "Tempore accusantium possimus aut libero.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 7, + "description": "Modi odio mollitia dolorem qui.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:twitter/typeahead-js.git", + "http_url_to_repo": "http://localhost:3000/twitter/typeahead-js.git", + "web_url": "http://localhost:3000/twitter/typeahead-js", + "name": "Typeahead.Js", + "name_with_namespace": "Twitter / Typeahead.Js", + "path": "typeahead-js", + "path_with_namespace": "twitter/typeahead-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:56.212Z", + "last_activity_at": "2016-01-13T20:27:51.496Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-01-13T20:19:54.480Z", + "updated_at": "2016-01-13T20:19:54.480Z", + "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": true, + "open_issues_count": 4, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 10, + "notification_level": 3 + } + } +}, { + "id": 6, + "description": "Omnis asperiores ipsa et beatae quidem necessitatibus quia.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:twitter/flight.git", + "http_url_to_repo": "http://localhost:3000/twitter/flight.git", + "web_url": "http://localhost:3000/twitter/flight", + "name": "Flight", + "name_with_namespace": "Twitter / Flight", + "path": "flight", + "path_with_namespace": "twitter/flight", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:54.754Z", + "last_activity_at": "2016-01-13T20:27:50.502Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-01-13T20:19:54.480Z", + "updated_at": "2016-01-13T20:19:54.480Z", + "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": true, + "open_issues_count": 4, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 10, + "notification_level": 3 + } + } +}, { + "id": 5, + "description": "Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-test.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-test.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-test", + "name": "Gitlab Test", + "name_with_namespace": "Gitlab Org / Gitlab Test", + "path": "gitlab-test", + "path_with_namespace": "gitlab-org/gitlab-test", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:53.202Z", + "last_activity_at": "2016-01-13T20:27:41.626Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 4, + "description": "Aut molestias quas est ut aperiam officia quod libero.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-shell.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-shell.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-shell", + "name": "Gitlab Shell", + "name_with_namespace": "Gitlab Org / Gitlab Shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab-org/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:51.882Z", + "last_activity_at": "2016-01-13T20:27:35.678Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 20, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 3, + "description": "Excepturi molestiae quia repellendus omnis est illo illum eligendi.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ci.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ci.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-ci", + "name": "Gitlab Ci", + "name_with_namespace": "Gitlab Org / Gitlab Ci", + "path": "gitlab-ci", + "path_with_namespace": "gitlab-org/gitlab-ci", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:50.346Z", + "last_activity_at": "2016-01-13T20:27:30.115Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 3, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 2, + "description": "Adipisci quaerat dignissimos enim sed ipsam dolorem quia.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ce.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ce.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-ce", + "name": "Gitlab Ce", + "name_with_namespace": "Gitlab Org / Gitlab Ce", + "path": "gitlab-ce", + "path_with_namespace": "gitlab-org/gitlab-ce", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:49.065Z", + "last_activity_at": "2016-01-13T20:26:58.454Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 30, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 1, + "description": "Vel voluptatem maxime saepe ex quia.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:documentcloud/underscore.git", + "http_url_to_repo": "http://localhost:3000/documentcloud/underscore.git", + "web_url": "http://localhost:3000/documentcloud/underscore", + "name": "Underscore", + "name_with_namespace": "Documentcloud / Underscore", + "path": "underscore", + "path_with_namespace": "documentcloud/underscore", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:45.862Z", + "last_activity_at": "2016-01-13T20:25:03.106Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 2, + "name": "Documentcloud", + "path": "documentcloud", + "owner_id": null, + "created_at": "2016-01-13T20:19:44.464Z", + "updated_at": "2016-01-13T20:19:44.464Z", + "description": "Aut impedit perferendis fuga et ipsa repellat cupiditate et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}] diff --git a/spec/javascripts/fixtures/static_fixtures.rb b/spec/javascripts/fixtures/static_fixtures.rb new file mode 100644 index 0000000000000000000000000000000000000000..4569f16f0cab9782a87812a28a2259d3c0ee739d --- /dev/null +++ b/spec/javascripts/fixtures/static_fixtures.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + before(:all) do + clean_frontend_fixtures('static/') + end + + fixtures_path = File.expand_path(JavaScriptFixturesHelpers::FIXTURE_PATH, Rails.root) + haml_fixtures = Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path| + file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '') + end + + haml_fixtures.each do |template_file_name| + it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example| + fixture_file_name = example.description + rendered = render_template(template_file_name) + store_frontend_fixture(rendered, fixture_file_name) + end + end + + private + + def render_template(template_file_name) + fixture_path = JavaScriptFixturesHelpers::FIXTURE_PATH + controller = ApplicationController.new + controller.prepend_view_path(fixture_path) + controller.render_to_string(template: template_file_name, layout: false) + end +end diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb new file mode 100644 index 0000000000000000000000000000000000000000..c9c0b89123766f4a83f535226955ac36e725b964 --- /dev/null +++ b/spec/javascripts/fixtures/u2f.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +context 'U2F' do + include JavaScriptFixturesHelpers + + let(:user) { create(:user, :two_factor_via_u2f) } + + before(:all) do + clean_frontend_fixtures('u2f/') + end + + describe SessionsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + end + + it 'u2f/authenticate.html.raw' do |example| + allow(controller).to receive(:find_user).and_return(user) + + post :create, user: { login: user.username, password: user.password } + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(user) + end + + it 'u2f/register.html.raw' do |example| + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml deleted file mode 100644 index 779d6429a5fe63fbfb25c3d6e5ab02980b9c75c2..0000000000000000000000000000000000000000 --- a/spec/javascripts/fixtures/u2f/authenticate.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" } diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml deleted file mode 100644 index 5ed51be689cda34401d273ae599185eb90f0bbde..0000000000000000000000000000000000000000 --- a/spec/javascripts/fixtures/u2f/register.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- user = FactoryGirl.build(:user, :two_factor_via_otp) -= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f', current_user: user } diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 8ba238018cdd4775ba88424f354d0bc73a6c67a8..d11b1182d9a18a0ba54729dacba301633e6d0e13 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ +/* global Turbolinks */ + /*= require jquery */ /*= require gl_dropdown */ /*= require turbolinks */ @@ -41,14 +43,16 @@ } describe('Dropdown', function describeDropdown() { - fixture.preload('gl_dropdown.html'); - fixture.preload('projects.json'); + preloadFixtures('static/gl_dropdown.html.raw'); function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ selectable: true, filterable: isFilterable, data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, + search: { + fields: ['name'] + }, text: (project) => { (project.name_with_namespace || project.name); }, @@ -59,10 +63,10 @@ } beforeEach(() => { - fixture.load('gl_dropdown.html'); + loadFixtures('static/gl_dropdown.html.raw'); this.dropdownContainerElement = $('.dropdown.inline'); this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); - this.projectsData = fixture.load('projects.json')[0]; + this.projectsData = getJSONFixture('projects.json'); }); afterEach(() => { @@ -166,5 +170,21 @@ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); }); + + + it('should still have input value on close and restore', () => { + let $searchInput = $(SEARCH_INPUT_SELECTOR); + initDropDown.call(this, false, true); + $searchInput + .trigger('focus') + .val('g') + .trigger('input'); + expect($searchInput.val()).toEqual('g'); + this.dropdownButtonElement.trigger('hidden.bs.dropdown'); + $searchInput + .trigger('blur') + .trigger('focus'); + expect($searchInput.val()).toEqual('g'); + }); }); })(); diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 0713e30e485692b48e9ceb5fe9f098c47daf85ce..e5d934540af258e9c11ff86975aca48963cd4bf2 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,13 +1,14 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ + //= require jquery //= require gl_field_errors ((global) => { - fixture.preload('gl_field_errors.html'); + preloadFixtures('static/gl_field_errors.html.raw'); describe('GL Style Field Errors', function() { beforeEach(function() { - fixture.load('gl_field_errors.html'); + loadFixtures('static/gl_field_errors.html.raw'); const $form = this.$form = $('form.gl-show-field-errors'); this.fieldErrors = new global.GlFieldErrors($form); }); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a406e6cc36a200ebca50ee5a8d4ff71c24bca29d..bc5cbeb6a4081f71181788208a6f781626cd203e 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,8 @@ -/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* global d3 */ +/* global ContributorsGraph */ +/* global ContributorsMasterGraph */ + //= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 96f39abe13ec930f5c1a12c55dde5f3c6aabca99..751f3d175e2560c44bee70bc20e1b1efa61b57c8 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */ +/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ +/* global ContributorsStatGraphUtil */ + //= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index f78573b992bdd1c25e1f25dcb1465cd24443cfff..0da124632ae751fd0a7fdfb1aafe9ca492a89287 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-undef, semi */ +/* eslint-disable quotes, padded-blocks, semi */ +/* global StatGraph */ + //= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index d2bcbc37b649b2bd08932143e4661aa356e2c97b..b5262afa1cfb4406b70338cc13bd8797cca8d256 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -7,7 +7,7 @@ describe('Header', function() { var todosPendingCount = '.todos-pending-count'; - var fixtureTemplate = 'header.html'; + var fixtureTemplate = 'static/header.html.raw'; function isTodosCountHidden() { return $(todosPendingCount).hasClass('hidden'); @@ -17,9 +17,9 @@ $(document).trigger('todo:toggle', newCount); } - fixture.preload(fixtureTemplate); + preloadFixtures(fixtureTemplate); beforeEach(function() { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); }); it('should update todos-pending-count after receiving the todo:toggle event', function() { diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..917a6267b9229e1ddd197c981aa946bcfadb7bb7 --- /dev/null +++ b/spec/javascripts/issuable_spec.js.es6 @@ -0,0 +1,81 @@ +/* global Issuable */ +/* global Turbolinks */ + +//= require issuable +//= require turbolinks + +(() => { + const BASE_URL = '/user/project/issues?scope=all&state=closed'; + const DEFAULT_PARAMS = '&utf8=%E2%9C%93'; + + function updateForm(formValues, form) { + $.each(formValues, (id, value) => { + $(`#${id}`, form).val(value); + }); + } + + function resetForm(form) { + $('input[name!="utf8"]', form).each((index, input) => { + input.setAttribute('value', ''); + }); + } + + describe('Issuable', () => { + preloadFixtures('static/issuable_filter.html.raw'); + + beforeEach(() => { + loadFixtures('static/issuable_filter.html.raw'); + Issuable.init(); + }); + + it('should be defined', () => { + expect(window.Issuable).toBeDefined(); + }); + + describe('filtering', () => { + let $filtersForm; + + beforeEach(() => { + $filtersForm = $('.js-filter-form'); + loadFixtures('static/issuable_filter.html.raw'); + resetForm($filtersForm); + }); + + it('should contain only the default parameters', () => { + spyOn(Turbolinks, 'visit'); + + Issuable.filterResults($filtersForm); + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); + }); + + it('should filter for the phrase "broken"', () => { + spyOn(Turbolinks, 'visit'); + + updateForm({ search: 'broken' }, $filtersForm); + Issuable.filterResults($filtersForm); + const params = `${DEFAULT_PARAMS}&search=broken`; + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + + it('should keep query parameters after modifying filter', () => { + spyOn(Turbolinks, 'visit'); + + // initial filter + updateForm({ milestone_title: 'v1.0' }, $filtersForm); + + Issuable.filterResults($filtersForm); + let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + + // update filter + updateForm({ label_name: 'Frontend' }, $filtersForm); + + Issuable.filterResults($filtersForm); + params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + }); + }); +})(); diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..a1e979e8d0993213e15aca80526273f7c588259b --- /dev/null +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -0,0 +1,201 @@ +/* eslint-disable */ +//= require jquery +//= require vue +//= require issuable/time_tracking/components/time_tracker + +function initTimeTrackingComponent(opts) { + fixture.set(` + <div> + <div id="mock-container"></div> + </div> + `); + + this.initialData = { + time_estimate: opts.timeEstimate, + time_spent: opts.timeSpent, + human_time_estimate: opts.timeEstimateHumanReadable, + human_time_spent: opts.timeSpentHumanReadable, + docsUrl: '/help/workflow/time_tracking.md', + }; + + const TimeTrackingComponent = Vue.component('issuable-time-tracker'); + this.timeTracker = new TimeTrackingComponent({ + el: '#mock-container', + propsData: this.initialData, + }); +} + +((gl) => { + describe('Issuable Time Tracker', function() { + describe('Initialization', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); + }); + + it('should return something defined', function() { + expect(this.timeTracker).toBeDefined(); + }); + + it ('should correctly set timeEstimate', function(done) { + Vue.nextTick(() => { + expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate); + done(); + }); + }); + it ('should correctly set time_spent', function(done) { + Vue.nextTick(() => { + expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent); + done(); + }); + }); + }); + + describe('Content Display', function() { + describe('Panes', function() { + describe('Comparison pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' }); + }); + + it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) { + Vue.nextTick(() => { + const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane'); + expect(this.timeTracker.showComparisonState).toBe(true); + done(); + }); + }); + + describe('Remaining meter', function() { + it('should display the remaining meter with the correct width', function(done) { + Vue.nextTick(() => { + const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width; + const correctWidth = '5%'; + + expect(meterWidth).toBe(correctWidth); + done(); + }) + }); + + it('should display the remaining meter with the correct background color when within estimate', function(done) { + Vue.nextTick(() => { + const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill'); + expect(styledMeter.length).toBe(1); + done() + }); + }); + + it('should display the remaining meter with the correct background color when over estimate', function(done) { + this.timeTracker.time_estimate = 100000; + this.timeTracker.time_spent = 20000000; + Vue.nextTick(() => { + const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill'); + expect(styledMeter.length).toBe(1); + done(); + }); + }); + }); + }); + + describe("Estimate only pane", function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' }); + }); + + it('should display the human readable version of time estimated', function(done) { + Vue.nextTick(() => { + const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText; + const correctText = 'Estimated: 2h 46m'; + + expect(estimateText).toBe(correctText); + done(); + }); + }); + }); + + describe('Spent only pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); + }); + + it('should display the human readable version of time spent', function(done) { + Vue.nextTick(() => { + const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText; + const correctText = 'Spent: 1h 23m'; + + expect(spentText).toBe(correctText); + done(); + }); + }); + }); + + describe('No time tracking pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 }); + }); + + it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) { + Vue.nextTick(() => { + const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane'); + const noTrackingText =$noTrackingPane.innerText; + const correctText = 'No estimate or time spent'; + + expect(this.timeTracker.showNoTimeTrackingState).toBe(true); + expect($noTrackingPane).toBeVisible(); + expect(noTrackingText).toBe(correctText); + done(); + }); + }); + }); + + describe("Help pane", function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 }); + }); + + it('should not show the "Help" pane by default', function(done) { + Vue.nextTick(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + + expect(this.timeTracker.showHelpState).toBe(false); + expect($helpPane).toBeNull(); + done(); + }); + }); + + it('should show the "Help" pane when help button is clicked', function(done) { + Vue.nextTick(() => { + $(this.timeTracker.$el).find('.help-button').click(); + + setTimeout(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + expect(this.timeTracker.showHelpState).toBe(true); + expect($helpPane).toBeVisible(); + done(); + }, 10); + }); + }); + + it('should not show the "Help" pane when help button is clicked and then closed', function(done) { + Vue.nextTick(() => { + $(this.timeTracker.$el).find('.help-button').click(); + + setTimeout(() => { + + $(this.timeTracker.$el).find('.close-help-button').click(); + + setTimeout(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + + expect(this.timeTracker.showHelpState).toBe(false); + expect($helpPane).toBeNull(); + + done(); + }, 1000); + }, 1000); + }); + }); + }); + }); + }); + }); +})(window.gl || (window.gl = {})); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 14af6644de1a0ecb0492c0ed474f914c59a9af60..eb07421826c95e053eb4cbbd7e277da354f3c7d7 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* global Issue */ /*= require lib/utils/text_utility */ /*= require issue */ @@ -7,9 +8,9 @@ var INVALID_URL = 'http://goesnowhere.nothing/whereami'; var $boxClosed, $boxOpen, $btnClose, $btnReopen; - fixture.preload('issues/closed-issue.html'); - fixture.preload('issues/issue-with-task-list.html'); - fixture.preload('issues/open-issue.html'); + preloadFixtures('issues/closed-issue.html.raw'); + preloadFixtures('issues/issue-with-task-list.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); function expectErrorMessage() { var $flashMessage = $('div.flash-alert'); @@ -60,8 +61,8 @@ describe('Issue', function() { describe('task lists', function() { - fixture.load('issues/issue-with-task-list.html'); beforeEach(function() { + loadFixtures('issues/issue-with-task-list.html.raw'); this.issue = new Issue(); }); @@ -70,7 +71,7 @@ $('input[type=checkbox]').attr('checked', true).trigger('change'); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - + it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); @@ -85,7 +86,7 @@ describe('close issue', function() { beforeEach(function() { - fixture.load('issues/open-issue.html'); + loadFixtures('issues/open-issue.html.raw'); findElements(); this.issue = new Issue(); @@ -139,7 +140,7 @@ describe('reopen issue', function() { beforeEach(function() { - fixture.load('issues/closed-issue.html'); + loadFixtures('issues/closed-issue.html.raw'); findElements(); this.issue = new Issue(); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 49687048eb5ab12f101eb493090c7074cf157b6e..e3146559a4a44f1bf4f636091d8d8a3761ce98ba 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */ +/* global IssuableContext */ +/* global LabelsSelect */ + //= require lib/utils/type_utility //= require jquery //= require bootstrap @@ -14,10 +17,10 @@ (() => { let saveLabelCount = 0; describe('Issue dropdown sidebar', () => { - fixture.preload('issue_sidebar_label.html'); + preloadFixtures('static/issue_sidebar_label.html.raw'); beforeEach(() => { - fixture.load('issue_sidebar_label.html'); + loadFixtures('static/issue_sidebar_label.html.raw'); new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); new LabelsSelect(); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index ef75f6008985aaa55ae506631e2e8bc3aca0a63f..031f9ca03c96361492aed9e724925e759af05442 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -15,6 +15,7 @@ expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); }); }); + describe('gl.utils.parseUrlPathname', () => { beforeEach(() => { spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ @@ -28,5 +29,28 @@ expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); }); }); + + describe('gl.utils.getUrlParamsArray', () => { + it('should return params array', () => { + expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = gl.utils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); + }); + + describe('gl.utils.getParameterByName', () => { + it('should return valid parameter', () => { + const value = gl.utils.getParameterByName('reporter'); + expect(value).toBe('Console'); + }); + + it('should return invalid parameter', () => { + const value = gl.utils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); + }); }); })(); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..e97356b65d5510088261738640d8dbd30dcd65a3 --- /dev/null +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -0,0 +1,25 @@ +//= require lib/utils/text_utility + +(() => { + describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); + + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); + + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); + + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); + }); + }); + }); +})(); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index b8b174a2e53a6ebeb1dc8c5b07120cf404b1d8fa..31f516b41bfc874f7d19c2dc006d405b1aba67d2 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,11 +1,12 @@ -/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* global LineHighlighter */ /*= require line_highlighter */ (function() { describe('LineHighlighter', function() { var clickLine; - fixture.preload('line_highlighter.html'); + preloadFixtures('static/line_highlighter.html.raw'); clickLine = function(number, eventData) { var e; if (eventData == null) { @@ -19,7 +20,7 @@ } }; beforeEach(function() { - fixture.load('line_highlighter.html'); + loadFixtures('static/line_highlighter.html.raw'); this["class"] = new LineHighlighter(); this.css = this["class"].highlightClass; return this.spies = { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index cbe2634d3a43f6b9b9cf557274939b7610ffe347..9b232617fe53abb9467f38697ea3362f617fb122 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,13 +1,14 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */ +/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ +/* global MergeRequest */ /*= require merge_request */ (function() { describe('MergeRequest', function() { return describe('task lists', function() { - fixture.preload('merge_requests_show.html'); + preloadFixtures('static/merge_requests_show.html.raw'); beforeEach(function() { - fixture.load('merge_requests_show.html'); + loadFixtures('static/merge_requests_show.html.raw'); return this.merge = new MergeRequest(); }); it('modifies the Markdown field', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 130d391bfab30b15136b3982ef1058b7985e73a5..98201fb98ed5afd4461d04cd810507b41593e0b1 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -16,7 +16,7 @@ }; $.extend(stubLocation, defaults, stubs || {}); }; - fixture.preload('merge_request_tabs.html'); + preloadFixtures('static/merge_request_tabs.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -30,7 +30,7 @@ describe('#activateTab', function () { beforeEach(function () { spyOn($, 'ajax').and.callFake(function () {}); - fixture.load('merge_request_tabs.html'); + loadFixtures('static/merge_request_tabs.html.raw'); this.subject = this.class.activateTab; }); it('shows the first tab when action is show', function () { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..a1c2fe3df374e9cad2a71df9bd7fb29ffca55707 --- /dev/null +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -0,0 +1,51 @@ +/* eslint-disable no-new */ + +//= require flash +//= require mini_pipeline_graph_dropdown + +(() => { + describe('Mini Pipeline Graph Dropdown', () => { + preloadFixtures('static/mini_dropdown_graph.html.raw'); + + beforeEach(() => { + loadFixtures('static/mini_dropdown_graph.html.raw'); + }); + + describe('When is initialized', () => { + it('should initialize without errors when no options are given', () => { + const miniPipelineGraph = new window.gl.MiniPipelineGraph(); + + expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); + }); + + it('should set the container as the given prop', () => { + const container = '.foo'; + + const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); + + expect(miniPipelineGraph.container).toEqual(container); + }); + }); + + describe('When dropdown is clicked', () => { + it('should call getBuildsList', () => { + const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + + document.querySelector('.js-builds-dropdown-button').click(); + + expect(getBuildsListSpy).toHaveBeenCalled(); + }); + + it('should make a request to the endpoint provided in the html', () => { + const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + + document.querySelector('.js-builds-dropdown-button').click(); + expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + }); + }); + }); +})(); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8828970d984d22bd1edddb3dab8eaaf4f61d9f06..e0dc549a9f4eaa306d88a65becf72171598ca907 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ +/* global NewBranchForm */ /*= require jquery-ui/autocomplete */ /*= require new_branch_form */ @@ -7,7 +8,7 @@ describe('Branch', function() { return describe('create a new branch', function() { var expectToHaveError, fillNameWith; - fixture.preload('new_branch.html'); + preloadFixtures('static/new_branch.html.raw'); fillNameWith = function(value) { return $('.js-branch-name').val(value).trigger('blur'); }; @@ -15,7 +16,7 @@ return expect($('.js-branch-name-error span').text()).toEqual(error); }; beforeEach(function() { - fixture.load('new_branch.html'); + loadFixtures('static/new_branch.html.raw'); $('form').on('submit', function(e) { return e.preventDefault(); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2db182d702b6a56962703f0f93efd546e0921f90..9cdb0a5d5aaf9b8351dbbaa7d9069d0b0e969e8c 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* global Notes */ + /*= require notes */ /*= require autosize */ /*= require gl_form */ @@ -10,13 +12,14 @@ gl.utils = gl.utils || {}; describe('Notes', function() { - var commentsTemplate = 'issues/issue_with_comment.raw'; - fixture.preload(commentsTemplate); + var commentsTemplate = 'issues/issue_with_comment.html.raw'; + preloadFixtures(commentsTemplate); beforeEach(function () { - fixture.load(commentsTemplate); + loadFixtures(commentsTemplate); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; + $('body').data('page', 'projects:issues:show'); }); describe('task lists', function() { diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index 85c9cf4b4f1d0f3f2c1090b38c3a66d1eaa22a85..f0f9ad7430d5ed5b492807539ffad030b2b4f4e3 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -2,10 +2,10 @@ (() => { describe('Pipelines', () => { - fixture.preload('pipeline_graph'); + preloadFixtures('static/pipeline_graph.html.raw'); beforeEach(() => { - fixture.load('pipeline_graph'); + loadFixtures('static/pipeline_graph.html.raw'); }); it('should be defined', () => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 2e12d45f7a74c6c7f3fd1fbd9fe3825b51d72c7f..7a04fba5f7f17f4ebfc983c587597e3534f14f3d 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,12 +1,12 @@ //= require lib/utils/pretty_time (() => { - const PrettyTime = gl.PrettyTime; + const prettyTime = gl.utils.prettyTime; - describe('PrettyTime methods', function () { + describe('prettyTime methods', function () { describe('parseSeconds', function () { it('should correctly parse a negative value', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const zeroSeconds = parser(-1000); @@ -17,7 +17,7 @@ }); it('should correctly parse a zero value', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const zeroSeconds = parser(0); @@ -28,7 +28,7 @@ }); it('should correctly parse a small non-zero second values', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const subOneMinute = parser(10); @@ -53,7 +53,7 @@ }); it('should correctly parse large second values', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const aboveOneHour = parser(4800); @@ -87,7 +87,7 @@ minutes: 20, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('1w 4d 7h 20m'); }); @@ -100,7 +100,7 @@ minutes: 20, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('4d 20m'); }); @@ -113,7 +113,7 @@ minutes: 0, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('0m'); }); @@ -122,12 +122,12 @@ describe('abbreviateTime', function () { it('should abbreviate stringified times for weeks', function () { const fullTimeString = '1w 3d 4h 5m'; - expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w'); + expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w'); }); it('should abbreviate stringified times for non-weeks', function () { const fullTimeString = '0w 3d 4h 5m'; - expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d'); + expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d'); }); }); }); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 65de1756201f6eaaf56ba426172f8b2ff18ad0f7..27b071f266d4d8e2b66b2cf0071fb3475193d9cd 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ + +/* global Project */ /*= require bootstrap */ /*= require select2 */ @@ -14,16 +16,16 @@ window.gon.api_version = 'v3'; describe('Project Title', function() { - fixture.preload('project_title.html'); - fixture.preload('projects.json'); + preloadFixtures('static/project_title.html.raw'); beforeEach(function() { - fixture.load('project_title.html'); + loadFixtures('static/project_title.html.raw'); return this.project = new Project(); }); return describe('project list', function() { var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); + expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 }); d = $.Deferred(); d.resolve(this.projects_data); return d.promise(); @@ -31,7 +33,7 @@ beforeEach((function(_this) { return function() { - _this.projects_data = fixture.load('projects.json')[0]; + _this.projects_data = getJSONFixture('projects.json'); return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 0a9bc546144c3dacbc4a3472aa0b2ab3693711a2..0177d8e4e7992940b16989b43a60d7a7088f501f 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* global Sidebar */ /*= require right_sidebar */ /*= require jquery */ @@ -35,9 +36,9 @@ describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; - fixture.preload(fixtureName); + preloadFixtures(fixtureName); beforeEach(function() { - fixture.load(fixtureName); + loadFixtures(fixtureName); this.sidebar = new Sidebar; $aside = $('.right-sidebar'); $page = $('.page-with-sidebar'); @@ -64,9 +65,10 @@ }); it('should broadcast todo:toggle event when add todo clicked', function() { + var todos = getJSONFixture('todos.json'); spyOn(jQuery, 'ajax').and.callFake(function() { var d = $.Deferred(); - var response = fixture.load('todos.json'); + var response = todos; d.resolve(response); return d.promise(); }); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 1b7f642d59e0af330e75fdd52c4915cd0c08b161..2d3f44e7980dd398f08aa6d436d6aa154d4f05e8 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -11,6 +11,7 @@ (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + var userName = 'root'; widget = null; @@ -19,6 +20,7 @@ window.gon || (window.gon = {}); window.gon.current_user_id = userId; + window.gon.current_username = userName; dashboardIssuesPath = '/dashboard/issues'; @@ -93,8 +95,8 @@ assertLinks = function(list, issuesPath, mrsPath) { var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; - issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; - issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; + issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; a1 = "a[href='" + issuesAssignedToMeLink + "']"; @@ -112,9 +114,9 @@ }; describe('Search autocomplete dropdown', function() { - fixture.preload('search_autocomplete.html'); + preloadFixtures('static/search_autocomplete.html.raw'); beforeEach(function() { - fixture.load('search_autocomplete.html'); + loadFixtures('static/search_autocomplete.html.raw'); return widget = new gl.SearchAutocomplete; }); it('should show Dashboard specific dropdown menu', function() { diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index e37816b0a8cfc62b8b1bad30d6a54e7a71efbe80..ae5d639ad9c9cf2735fb2a3c23ddac83da40c3c8 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,13 +1,14 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ +/* global ShortcutsIssuable */ /*= require shortcuts_issuable */ (function() { describe('ShortcutsIssuable', function() { var fixtureName = 'issues/open-issue.html.raw'; - fixture.preload(fixtureName); + preloadFixtures(fixtureName); beforeEach(function() { - fixture.load(fixtureName); + loadFixtures(fixtureName); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); return this.shortcut = new ShortcutsIssuable(); }); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index 9a9fb22255b9d7c3f52e440eb4799965d0568cdc..c274b9c45f448f29111a56a43603f9104f886f1d 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -2,7 +2,7 @@ ((global) => { describe('SigninTabsMemoizer', () => { - const fixtureTemplate = 'signin_tabs.html'; + const fixtureTemplate = 'static/signin_tabs.html.raw'; const tabSelector = 'ul.nav-tabs'; const currentTabKey = 'current_signin_tab'; let memo; @@ -15,10 +15,10 @@ return memo; } - fixture.preload(fixtureTemplate); + preloadFixtures(fixtureTemplate); beforeEach(() => { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); }); it('does nothing if no tab was previously selected', () => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 1b7ca97cde48c6e071e012c5b40a0e76f3465c76..39d236986b982ae194843be373e927a16e687087 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -103,7 +103,7 @@ describe('DOM Events', function () { beforeEach(function () { // This ensures DOM and DOM events are initialized for these specs. - fixture.set('<div></div>'); + setFixtures('<div></div>'); this.smartInterval = createDefaultSmartInterval(); }); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 831dfada95224abb52880d14d150a352e50ff915..f8e3aca29fa5269614e5d704c030890d16806961 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -37,12 +37,12 @@ // file as a manifest. // For more information: http://github.com/modeset/teaspoon -(function() { - - -}).call(this); +// set our fixtures path +jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures'; +jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures'; // defined in ActionDispatch::TestRequest // see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 window.gl = window.gl || {}; -gl.TEST_HOST = 'http://test.host'; +window.gl.TEST_HOST = 'http://test.host'; +window.gon = window.gon || {}; diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index df395296791a5c908ce638f4149e5a3861bfbd4c..6a70dd856a711e656a5a54bf17fa57b34f183ce9 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ + //= vue //= vue-resource //= require jquery diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index ac411f6c3060801fbd6efbf92886845c46304d69..5984ce8ffd4e1be190e049b997a69ca23b99ee44 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -13,7 +13,7 @@ }; describe('on a js-syntax-highlight element', function() { beforeEach(function() { - return fixture.set('<div class="js-syntax-highlight"></div>'); + return setFixtures('<div class="js-syntax-highlight"></div>'); }); return it('applies syntax highlighting', function() { stubUserColorScheme('monokai'); @@ -23,7 +23,7 @@ }); return describe('on a parent element', function() { beforeEach(function() { - return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); }); it('applies highlighting to all applicable children', function() { stubUserColorScheme('monokai'); @@ -33,7 +33,7 @@ }); return it('prevents an infinite loop when no matches exist', function() { var highlight; - fixture.set('<div></div>'); + setFixtures('<div></div>'); highlight = function() { return $('div').syntaxHighlight(); }; diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 944df6d23f7bc4c3b683f97cc21df24c909fcb9e..dc2f49679853502412c1346c3b677d9eb703bb9a 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FAuthenticate */ /*= require u2f/authenticate */ /*= require u2f/util */ @@ -8,22 +10,25 @@ (function() { describe('U2FAuthenticate', function() { - fixture.load('u2f/authenticate'); + preloadFixtures('u2f/authenticate.html.raw'); + beforeEach(function() { + loadFixtures('u2f/authenticate.html.raw'); this.u2fDevice = new MockU2FDevice; this.container = $("#js-authenticate-u2f"); - this.component = new U2FAuthenticate(this.container, { - sign_requests: [] - }, "token"); + this.component = new window.gl.U2FAuthenticate( + this.container, + '#js-login-u2f-form', + { + sign_requests: [] + }, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form') + ); return this.component.start(); }); it('allows authenticating via a U2F device', function() { - var authenticatedMessage, deviceResponse, inProgressMessage, setupButton, setupMessage; - setupButton = this.container.find("#js-login-u2f-device"); - setupMessage = this.container.find("p"); - expect(setupMessage.text()).toContain('Insert your security key'); - expect(setupButton.text()).toBe('Sign in via U2F device'); - setupButton.trigger('click'); + var authenticatedMessage, deviceResponse, inProgressMessage; inProgressMessage = this.container.find("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); this.u2fDevice.respondToAuthenticateRequest({ @@ -31,7 +36,7 @@ }); authenticatedMessage = this.container.find("p"); deviceResponse = this.container.find('#js-device-response'); - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.'); return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); }); return describe("errors", function() { @@ -60,7 +65,7 @@ deviceData: "this is data from the device" }); authenticatedMessage = this.container.find("p"); - return expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated."); }); }); }); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0c73c5772bd09470d7c7776e4ef49932b29d56ac..ab4c5edd044b4fd9baa185c43c8df76f37a62af9 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FRegister */ /*= require u2f/register */ /*= require u2f/util */ @@ -8,8 +10,10 @@ (function() { describe('U2FRegister', function() { - fixture.load('u2f/register'); + preloadFixtures('u2f/register.html.raw'); + beforeEach(function() { + loadFixtures('u2f/register.html.raw'); this.u2fDevice = new MockU2FDevice; this.container = $("#js-register-u2f"); this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 26dfdb94aae490a2d6ba17d4e62b1b636270eb75..d6c6f786fb16b9ccacdd52b8deed51a631ce8ad2 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -5,7 +5,7 @@ describe('Commit component', () => { let component; it('should render a code-fork icon if it does not represent a tag', () => { - fixture.set('<div class="test-commit-container"></div>'); + setFixtures('<div class="test-commit-container"></div>'); component = new window.gl.CommitComponent({ el: document.querySelector('.test-commit-container'), propsData: { @@ -30,7 +30,7 @@ describe('Commit component', () => { describe('Given all the props', () => { beforeEach(() => { - fixture.set('<div class="test-commit-container"></div>'); + setFixtures('<div class="test-commit-container"></div>'); props = { tag: true, @@ -105,7 +105,7 @@ describe('Commit component', () => { describe('When commit title is not provided', () => { it('should render default message', () => { - fixture.set('<div class="test-commit-container"></div>'); + setFixtures('<div class="test-commit-container"></div>'); props = { tag: false, commitRef: { diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..1a7f2bb5fb8c367c7d26953b842ca66ad74d29c2 --- /dev/null +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -0,0 +1,168 @@ +//= require vue +//= require lib/utils/common_utils +//= require vue_pagination/index +/* global fixture, gl */ + +describe('Pagination component', () => { + let component; + + const changeChanges = { + one: '', + two: '', + }; + + const change = (one, two) => { + changeChanges.one = one; + changeChanges.two = two; + }; + + it('should render and start at page 1', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + expect(component.$el.classList).toContain('gl-pagination'); + + component.changePage({ target: { innerText: '1' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the previous page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 3, + previousPage: 1, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Prev' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the next page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Next' } }); + + expect(changeChanges.one).toEqual(5); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the last page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Last >>' } }); + + expect(changeChanges.one).toEqual(10); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the first page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: '<< First' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should do nothing', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + component.changePage({ target: { innerText: '...' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); +}); + +describe('paramHelper', () => { + it('can parse url parameters correctly', () => { + window.history.pushState({}, null, '?scope=all&p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual('all'); + expect(p).toEqual('2'); + }); + + it('returns null if param not in url', () => { + window.history.pushState({}, null, '?p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual(null); + expect(p).toEqual('2'); + }); +}); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index b9acaaa5a0da306c6f57c6746b34e39dc64c1268..f1c2edcc55c09121d9a507013105d0c5ad62e14a 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,7 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ +/* global ZenMode */ /*= require zen_mode */ @@ -7,9 +10,9 @@ describe('ZenMode', function() { var fixtureName = 'issues/open-issue.html.raw'; - fixture.preload(fixtureName); + preloadFixtures(fixtureName); beforeEach(function() { - fixture.load(fixtureName); + loadFixtures(fixtureName); spyOn(Dropzone, 'forElement').and.callFake(function() { return { enable: function() { @@ -29,9 +32,9 @@ return expect(Mousetrap.pause).toHaveBeenCalled(); }); return it('removes textarea styling', function() { - $('textarea').attr('style', 'height: 400px'); + $('.notes-form textarea').attr('style', 'height: 400px'); enterZen(); - return expect('textarea').not.toHaveAttr('style'); + return expect($('.notes-form textarea')).not.toHaveAttr('style'); }); }); describe('in use', function() { @@ -40,7 +43,7 @@ }); return it('exits on Escape', function() { escapeKeydown(); - return expect($('.zen-backdrop')).not.toHaveClass('fullscreen'); + return expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen'); }); }); return describe('on exit', function() { @@ -61,15 +64,15 @@ }); enterZen = function() { - return $('.js-zen-enter').click(); + return $('.notes-form .js-zen-enter').click(); }; - exitZen = function() { // Ohmmmmmmm - return $('.js-zen-leave').click(); + exitZen = function() { + return $('.notes-form .js-zen-leave').click(); }; escapeKeydown = function() { - return $('textarea').trigger($.Event('keydown', { + return $('.notes-form textarea').trigger($.Event('keydown', { keyCode: 27 })); }; diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..267318faed444c84133afb22ca8d40b6691f7741 --- /dev/null +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe API::Helpers::Pagination do + let(:resource) { Project.all } + + subject do + Class.new.include(described_class).new + end + + describe '#paginate' do + let(:value) { spy('return value') } + + before do + allow(value).to receive(:to_query).and_return(value) + + allow(subject).to receive(:header).and_return(value) + allow(subject).to receive(:params).and_return(value) + allow(subject).to receive(:request).and_return(value) + end + + describe 'required instance methods' do + let(:return_spy) { spy } + + it 'requires some instance methods' do + expect_message(:header) + expect_message(:params) + expect_message(:request) + + subject.paginate(resource) + end + end + + context 'when resource can be paginated' do + before do + create_list(:empty_project, 3) + end + + describe 'first page' do + before do + allow(subject).to receive(:params) + .and_return({ page: 1, per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 2 + end + + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + expect_header('X-Next-Page', '2') + expect_header('X-Prev-Page', '') + expect_header('Link', any_args) + + subject.paginate(resource) + end + end + + describe 'second page' do + before do + allow(subject).to receive(:params) + .and_return({ page: 2, per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 1 + end + + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '2') + expect_header('X-Next-Page', '') + expect_header('X-Prev-Page', '1') + expect_header('Link', any_args) + + subject.paginate(resource) + end + end + end + + def expect_header(name, value) + expect(subject).to receive(:header).with(name, value) + end + + def expect_message(method) + expect(subject).to receive(method) + .at_least(:once).and_return(value) + end + end +end diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb deleted file mode 100644 index 70a87fbc01e04ed219dcc708bcb6afeda3e51619..0000000000000000000000000000000000000000 --- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::AbstractReferenceFilter do - let(:project) { create(:empty_project) } - - describe '#references_per_project' do - it 'returns a Hash containing references grouped per project paths' do - doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) - expect(filter).to receive(:object_sym).twice.and_return(:issue) - - refs = filter.references_per_project - - expect(refs).to be_an_instance_of(Hash) - expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) - end - end - - describe '#projects_per_reference' do - it 'returns a Hash containing projects grouped per project paths' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:references_per_project). - and_return({ project.path_with_namespace => Set.new(%w[1]) }) - - expect(filter.projects_per_reference). - to eq({ project.path_with_namespace => project }) - end - end - - describe '#find_projects_for_paths' do - it 'returns a list of Projects for a list of paths' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.find_projects_for_paths([project.path_with_namespace])). - to eq([project]) - end - end - - describe '#current_project_path' do - it 'returns the path of the current project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.current_project_path).to eq(project.path_with_namespace) - end - end -end diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..27684882435ff5cb68109bda62e2d9318324e4cb --- /dev/null +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe Banzai::Filter::AbstractReferenceFilter do + let(:project) { create(:empty_project) } + + describe '#references_per_project' do + it 'returns a Hash containing references grouped per project paths' do + doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) + expect(filter).to receive(:object_sym).twice.and_return(:issue) + + refs = filter.references_per_project + + expect(refs).to be_an_instance_of(Hash) + expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) + end + end + + describe '#projects_per_reference' do + it 'returns a Hash containing projects grouped per project paths' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:references_per_project). + and_return({ project.path_with_namespace => Set.new(%w[1]) }) + + expect(filter.projects_per_reference). + to eq({ project.path_with_namespace => project }) + end + end + + describe '#find_projects_for_paths' do + let(:doc) { Nokogiri::HTML.fragment('') } + let(:filter) { described_class.new(doc, project: project) } + + context 'with RequestStore disabled' do + it 'returns a list of Projects for a list of paths' do + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + + it "return an empty array for paths that don't exist" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + end + + context 'with RequestStore enabled' do + before do + RequestStore.begin! + end + + after do + RequestStore.end! + RequestStore.clear! + end + + it 'returns a list of Projects for a list of paths' do + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + + context "when no project with that path exists" do + it "returns no value" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + + it "adds the ref to the project refs cache" do + project_refs_cache = {} + allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache) + + filter.find_projects_for_paths(['nonexistent/project']) + + expect(project_refs_cache).to eq({ 'nonexistent/project' => nil }) + end + + context 'when the project refs cache includes nil values' do + before do + # adds { 'nonexistent/project' => nil } to cache + filter.project_from_ref_cached('nonexistent/project') + end + + it "return an empty array for paths that don't exist" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + end + end + end + end + + describe '#current_project_path' do + it 'returns the path of the current project' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter.current_project_path).to eq(project.path_with_namespace) + end + end +end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 167397c736bdd8e6b15355411ab3057b926f083b..d9e4525cb2857795c361fa34bcbfa0d8b5ba6360 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do expect(filter(act).to_html).to eq(exp) end end + + context 'for protocol-relative links' do + let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) } + + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end + end end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..51883782e193de3a41dfc1f9ad203b145c5f4ea4 --- /dev/null +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Banzai::Filter::MathFilter, lib: true do + include FilterSpecHelper + + it 'leaves regular inline code unchanged' do + input = "<code>2+2</code>" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter("$<code>2+2</code>$") + + expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>' + end + + it 'only removes surrounding dollar signs' do + doc = filter("test $<code>2+2</code>$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test ' + expect(after.to_s).to eq ' test' + end + + it 'only removes surrounding single dollar sign' do + doc = filter("test $$<code>2+2</code>$$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test $' + expect(after.to_s).to eq '$ test' + end + + it 'adds data-math-style inline attribute to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code['data-math-style']).to eq 'inline' + end + + it 'adds class code and math to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("code") + expect(code[:class]).to include("math") + end + + it 'adds js-render-math class to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("js-render-math") + end + + # Cases with faulty syntax. Should be a no-op + + it 'ignores cases with missing dolar sign at the end' do + input = "test $<code>2+2</code> test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores cases with missing dolar sign at the beginning' do + input = "test <code>2+2</code>$ test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if it is not adjacent' do + input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if they are inside another element' do + input = '<p>We check strictly <em>$</em><code>2+2</code><em>$</em></p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + # Display math + + it 'adds data-math-style display attribute to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre['data-math-style']).to eq 'display' + end + + it 'adds js-render-math class to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre[:class]).to include("js-render-math") + end + + it 'ignores code blocks that are not math' do + input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'requires the pre to contain both code and math' do + input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'dollar signs around to display math' do + doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$') + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq '$' + expect(after.to_s).to eq '$' + end +end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index 50a5d1a19ba04198b2f0464555751f18d972d3a4..0af36776a54f9bced08dc5dddefee83e417edfc4 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -12,7 +12,17 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do context 'when the link has a data-issue attribute' do before { link['data-external-issue'] = 123 } - it_behaves_like "referenced feature visibility", "issues" + levels = [ProjectFeature::DISABLED, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] + + levels.each do |level| + it "creates reference when the feature is #{level}" do + project.project_feature.update(issues_access_level: level) + + visible_nodes = subject.nodes_visible_to_user(user, [link]) + + expect(visible_nodes).to include(link) + end + end end end diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..015a7f80e03441d932696ff7f530c5e37f698d96 --- /dev/null +++ b/spec/lib/bitbucket/collection_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +# Emulates paginator. It returns 2 pages with results +class TestPaginator + def initialize + @current_page = 0 + end + + def items + @current_page += 1 + + raise StopIteration if @current_page > 2 + + ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"] + end +end + +describe Bitbucket::Collection do + it "iterates paginator" do + collection = described_class.new(TestPaginator.new) + + expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"]) + end +end diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..14faeb231a980cdc53130d71d1e5c1ba0842eba8 --- /dev/null +++ b/spec/lib/bitbucket/connection_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Bitbucket::Connection do + before do + allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: '')) + end + + describe '#get' do + it 'calls OAuth2::AccessToken::get' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true)) + + connection = described_class.new({}) + + connection.get('/users') + end + end + + describe '#expired?' do + it 'calls connection.expired?' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true) + + expect(described_class.new({}).expired?).to be_truthy + end + end + + describe '#refresh!' do + it 'calls connection.refresh!' do + response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil) + + expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response) + + described_class.new({}).refresh! + end + end +end diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..04d5a0470b1ca568861c2ddda714855c1d716498 --- /dev/null +++ b/spec/lib/bitbucket/page_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Bitbucket::Page do + let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } } + + before do + # Autoloading hack + Bitbucket::Representation::User.new({}) + end + + describe '#items' do + it 'returns collection of needed objects' do + page = described_class.new(response, :user) + + expect(page.items.first).to be_a(Bitbucket::Representation::User) + expect(page.items.count).to eq(1) + end + end + + describe '#attrs' do + it 'returns attributes' do + page = described_class.new(response, :user) + + expect(page.attrs.keys).to include(:pagelen, :next) + end + end + + describe '#next?' do + it 'returns true' do + page = described_class.new(response, :user) + + expect(page.next?).to be_truthy + end + + it 'returns false' do + response['next'] = nil + page = described_class.new(response, :user) + + expect(page.next?).to be_falsey + end + end + + describe '#next' do + it 'returns next attribute' do + page = described_class.new(response, :user) + + expect(page.next).to eq('') + end + end +end diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2c972da682e99d5d9511062a0fcca89818b477f4 --- /dev/null +++ b/spec/lib/bitbucket/paginator_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Bitbucket::Paginator do + let(:last_page) { double(:page, next?: false, items: ['item_2']) } + let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) } + + describe 'items' do + it 'return items and raises StopIteration in the end' do + paginator = described_class.new(nil, nil, nil) + + allow(paginator).to receive(:fetch_next_page).and_return(first_page) + expect(paginator.items).to match(['item_1']) + + allow(paginator).to receive(:fetch_next_page).and_return(last_page) + expect(paginator.items).to match(['item_2']) + + allow(paginator).to receive(:fetch_next_page).and_return(nil) + expect{ paginator.items }.to raise_error(StopIteration) + end + end +end diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fec243a9f9603f622a5439788234a6bbc04d7a1f --- /dev/null +++ b/spec/lib/bitbucket/representation/comment_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Comment do + describe '#author' do + it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#note' do + it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') } + it { expect(described_class.new({}).note).to be_nil } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) } + it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..20f47224aa8e44ab04f4c2cf65b6148586d040a9 --- /dev/null +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Issue do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#kind' do + it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') } + end + + describe '#milestone' do + it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') } + it { expect(described_class.new({}).milestone).to be_nil } + end + + describe '#author' do + it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#description' do + it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..673dcf22ce869892bdbcae24dd2c344e2d0b5893 --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequestComment do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#file_path' do + it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') } + end + + describe '#old_pos' do + it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) } + end + + describe '#new_pos' do + it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) } + end + + describe '#parent_id' do + it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) } + it { expect(described_class.new({}).parent_id).to be_nil } + end + + describe '#inline?' do + it { expect(described_class.new('inline' => {}).inline?).to be_truthy } + it { expect(described_class.new({}).inline?).to be_falsey } + end + + describe '#has_parent?' do + it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy } + it { expect(described_class.new({}).has_parent?).to be_falsey } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..30453528be4c2d491920c3945e1afe119d156304 --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequest do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#author' do + it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#description' do + it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') } + it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') } + it { expect(described_class.new({}).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#source_branch_name' do + it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil } + end + + describe '#source_branch_sha' do + it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil } + end + + describe '#target_branch_name' do + it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil } + end + + describe '#target_branch_sha' do + it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil } + end +end diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..adcd978e1b3b6dffb74c3b2d80684f88bbdad34c --- /dev/null +++ b/spec/lib/bitbucket/representation/repo_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Repo do + describe '#has_wiki?' do + it { expect(described_class.new({ 'has_wiki' => false }).has_wiki?).to be_falsey } + it { expect(described_class.new({ 'has_wiki' => true }).has_wiki?).to be_truthy } + end + + describe '#name' do + it { expect(described_class.new({ 'name' => 'test' }).name).to eq('test') } + end + + describe '#valid?' do + it { expect(described_class.new({ 'scm' => 'hg' }).valid?).to be_falsey } + it { expect(described_class.new({ 'scm' => 'git' }).valid?).to be_truthy } + end + + describe '#full_name' do + it { expect(described_class.new({ 'full_name' => 'test_full' }).full_name).to eq('test_full') } + end + + describe '#description' do + it { expect(described_class.new({ 'description' => 'desc' }).description).to eq('desc') } + end + + describe '#issues_enabled?' do + it { expect(described_class.new({ 'has_issues' => false }).issues_enabled?).to be_falsey } + it { expect(described_class.new({ 'has_issues' => true }).issues_enabled?).to be_truthy } + end + + describe '#owner_and_slug' do + it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) } + end + + describe '#owner' do + it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner).to eq('ben') } + end + + describe '#slug' do + it { expect(described_class.new({ 'full_name' => 'ben/test' }).slug).to eq('test') } + end + + describe '#clone_url' do + it 'builds url' do + data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } } + expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git') + end + end +end diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f79ff4edb7bd88d7d2268945b867d2397041c182 --- /dev/null +++ b/spec/lib/bitbucket/representation/user_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Bitbucket::Representation::User do + describe '#username' do + it 'returns correct value' do + user = described_class.new('username' => 'Ben') + + expect(user.username).to eq('Ben') + end + end +end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 898f1e84ab0679e229fbee42a1e20e50328290d9..0762fd7e56a007034af3c0f7e9bba10e66573d5f 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do expect(subject.convert("<")[:html]).to eq('<') end + it "replaces newlines with line break tags" do + expect(subject.convert("\n")[:html]).to eq('<br>') + end + + it "groups carriage returns with newlines" do + expect(subject.convert("\r\n")[:html]).to eq('<br>') + end + describe "incremental update" do shared_examples 'stateable converter' do let(:pass1) { subject.convert(pre_text) } diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ff5dcc06ab30781f9d6e7cc3b51e83b0323a9429..f824e2e1efece63ab8a1fb1ba7d25b4d983288e8 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -483,7 +483,7 @@ module Ci context 'when global variables are defined' do let(:variables) do - { VAR1: 'value1', VAR2: 'value2' } + { 'VAR1' => 'value1', 'VAR2' => 'value2' } end let(:config) do { @@ -495,18 +495,18 @@ module Ci it 'returns global variables' do expect(subject).to contain_exactly( - { key: :VAR1, value: 'value1', public: true }, - { key: :VAR2, value: 'value2', public: true } + { 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' } + { 'VAR1' => 'global1', 'VAR3' => 'global3' } end let(:job_variables) do - { VAR1: 'value1', VAR2: 'value2' } + { 'VAR1' => 'value1', 'VAR2' => 'value2' } end let(:config) do { @@ -518,9 +518,9 @@ module Ci 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 } + { key: 'VAR3', value: 'global3', public: true }, + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } ) end end @@ -535,13 +535,13 @@ module Ci context 'when syntax is correct' do let(:variables) do - { VAR1: 'value1', VAR2: 'value2' } + { 'VAR1' => 'value1', 'VAR2' => 'value2' } end it 'returns job variables' do expect(subject).to contain_exactly( - { key: :VAR1, value: 'value1', public: true }, - { key: :VAR2, value: 'value2', public: true } + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } ) end end @@ -549,7 +549,7 @@ module Ci context 'when syntax is incorrect' do context 'when variables defined but invalid' do let(:variables) do - [ :VAR1, 'value1', :VAR2, 'value2' ] + [ 'VAR1', 'value1', 'VAR2', 'value2' ] end it 'raises error' do @@ -769,6 +769,19 @@ module Ci expect(builds.first[:environment]).to eq(environment[:name]) expect(builds.first[:options]).to include(environment: environment) end + + context 'the url has a port as variable' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com:$PORT' } + end + + it 'allows a variable for the port' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + end end context 'when no environment is specified' do diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..87733d53e92b7f45953d2c2a2f3cacc13a86209c --- /dev/null +++ b/spec/lib/gitlab/allowable_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::Allowable do + subject do + Class.new.include(described_class).new + end + + describe '#can?' do + let(:user) { create(:user) } + + context 'when user is allowed to do something' do + let(:project) { create(:empty_project, :public) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be true + end + end + + context 'when user is not allowed to do something' do + let(:project) { create(:empty_project, :private) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be false + end + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 4aba783dc334a1732bc2d9f01e9dbbc43155988b..ba199917f5c82d8addf518810577f106ee280ffb 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -8,10 +8,14 @@ module Gitlab let(:html) { 'H<sub>2</sub>O' } context "without project" do + before do + allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) + end + it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS } @@ -27,7 +31,7 @@ module Gitlab it "merges the options with default ones" do expected_asciidoc_opts = { safe: :safe, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index c9d64e99f88f2d8756d9765c0241dd3e02b2af43..f251c0dd25a1d8cac3080d26e096a52b37627b6e 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do project.create_drone_ci_service(active: true) project.drone_ci_service.update(token: 'token') - ip = 'ip' - - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token') - expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token') + expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) end it 'recognizes master passwords' do user = create(:user, password: 'password') - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end it 'recognizes user lfs tokens' do user = create(:user) - ip = 'ip' token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) end it 'recognizes deploy key lfs tokens' do key = create(:deploy_key) - ip = 'ip' token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) end - it 'recognizes OAuth tokens' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) - ip = 'ip' + context "while using OAuth tokens as passwords" do + it 'succeeds for OAuth tokens with the `api` scope' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + end - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + it 'fails for OAuth tokens with other scopes' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end + end + + context "while using personal access tokens as passwords" do + it 'succeeds for personal access tokens with the `api` scope' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + end + + it 'fails for personal access tokens with other scopes' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end end it 'returns double nil for invalid credentials' do login = 'foo' - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) - expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new) end end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index 1b749d1bd39a3c5c195a169f115e782eb61341a3..f84782ab440b6f002a75741b4a3eb6f9c024c822 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -1,9 +1,27 @@ require 'spec_helper' describe Backup::Manager, lib: true do - describe '#remove_old' do - let(:progress) { StringIO.new } + include StubENV + + let(:progress) { StringIO.new } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + @old_progress = $progress # rubocop:disable Style/GlobalVars + $progress = progress # rubocop:disable Style/GlobalVars + end + + after do + $progress = @old_progress # rubocop:disable Style/GlobalVars + end + describe '#remove_old' do let(:files) do [ '1451606400_2016_01_01_gitlab_backup.tar', @@ -20,20 +38,6 @@ describe Backup::Manager, lib: true do allow(Dir).to receive(:glob).and_return(files) allow(FileUtils).to receive(:rm) allow(Time).to receive(:now).and_return(Time.utc(2016)) - - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - - @old_progress = $progress # rubocop:disable Style/GlobalVars - $progress = progress # rubocop:disable Style/GlobalVars - end - - after do - $progress = @old_progress # rubocop:disable Style/GlobalVars end context 'when keep_time is zero' do @@ -124,4 +128,82 @@ describe Backup::Manager, lib: true do end end end + + describe '#unpack' do + before do + allow(Dir).to receive(:chdir) + end + + context 'when there are no backup files in the directory' do + before do + allow(Dir).to receive(:glob).and_return([]) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('No backups found')) + end + end + + context 'when there are two backup files in the directory and BACKUP variable is not set' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar', + ] + ) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('Found more than one backup')) + end + end + + context 'when BACKUP variable is set to a non-existing file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(false) + + stub_env('BACKUP', 'wrong') + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar') + expect(progress).to have_received(:puts) + .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist')) + end + end + + context 'when BACKUP variable is set to a correct file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(true) + allow(Kernel).to receive(:system).and_return(true) + allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) + + stub_env('BACKUP', '1451606400_2016_01_01') + end + + it 'unpacks the file' do + subject.unpack + + expect(Kernel).to have_received(:system) + .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar") + expect(progress).to have_received(:puts).with(a_string_matching('done')) + end + end + end end diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 38eebb2a1769b2a25656e77b3c4239c08e9e25ca..70f03021d361f1cc965cf6b4eda6ad93fcde2b56 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do new_build.success! end - it 'reports the compound status' do - expect(badge.status).to eq 'failed' + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' end end end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb deleted file mode 100644 index 7543c29bcc449f5f94551b4cfc90f497415cfa5d..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Gitlab::BitbucketImport::Client, lib: true do - include ImportSpecHelper - - let(:token) { '123456' } - let(:secret) { 'secret' } - let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } - - before do - stub_omniauth_provider('bitbucket') - end - - it 'all OAuth client options are symbols' do - client.consumer.options.keys.each do |key| - expect(key).to be_kind_of(Symbol) - end - end - - context 'issues' do - let(:per_page) { 50 } - let(:count) { 95 } - let(:sample_issues) do - issues = [] - - count.times do |i| - issues << { local_id: i } - end - - issues - end - let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } } - let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } } - let(:project_id) { 'namespace/repo' } - - it 'retrieves issues over a number of pages' do - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0"). - to_return(status: 200, - body: first_sample_data.to_json, - headers: {}) - - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50"). - to_return(status: 200, - body: second_sample_data.to_json, - headers: {}) - - issues = client.issues(project_id) - expect(issues.count).to eq(95) - end - end - - 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" } }) - - expect { described_class.from_project(project) }.not_to raise_error - end - end -end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index aa00f32becbec1861e12a5cc6db056add55a804b..72b1ba36b58b417db60925b14dbb86befcb2de5a 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do "closed" # undocumented status ] end + let(:sample_issues_statuses) do issues = [] statuses.map.with_index do |status, index| issues << { - local_id: index, - status: status, + id: index, + state: status, title: "Issue #{index}", - content: "Some content to issue #{index}" + kind: 'bug', + content: { + raw: "Some content to issue #{index}", + markup: "markdown", + html: "Some content to issue #{index}" + } } end @@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do end let(:project_identifier) { 'namespace/repo' } + let(:data) do { 'bb_session' => { - 'bitbucket_access_token' => "123456", - 'bitbucket_access_token_secret' => "secret" + 'bitbucket_token' => "123456", + 'bitbucket_refresh_token' => "secret" } } end + let(:project) do create( :project, @@ -49,42 +57,70 @@ describe Gitlab::BitbucketImport::Importer, lib: true do import_data: ProjectImportData.new(credentials: data) ) end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do { count: sample_issues_statuses.count, - issues: sample_issues_statuses + values: sample_issues_statuses } end context 'issues statuses' do before do + # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this + Bitbucket::Representation::Issue.new({}) + stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}" - ).to_return(status: 200, body: { has_issues: true }.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: { has_issues: true, full_name: project_identifier }.to_json) stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0" - ).to_return(status: 200, body: issues_statuses_sample_data.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: issues_statuses_sample_data.to_json) + + stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, + body: "", + headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments" + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on" ).to_return( status: 200, - body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + headers: { "Content-Type" => "application/json" }, + body: { author_info: { username: "username" }, utc_created_on: index }.to_json ) end + + stub_request( + :get, + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: {}.to_json) end - it 'map statuses to open or closed' do + it 'maps statuses to open or closed' do importer.execute expect(project.issues.where(state: "closed").size).to eq(5) expect(project.issues.where(state: "opened").size).to eq(2) end + + it 'calls import_wiki' do + expect(importer).to receive(:import_wiki) + importer.execute + end end end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index e1c60e07b4d656f9b54dd7df45eac9aa69c4d191..773d0d4d288dc30d4b5f1349cff86a9ad95ae616 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -2,14 +2,19 @@ require 'spec_helper' describe Gitlab::BitbucketImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:repo) do - { - name: 'Vim', - slug: 'vim', - is_private: true, - owner: "asd" - }.with_indifferent_access + double(name: 'Vim', + slug: 'vim', + description: 'Test repo', + is_private: true, + owner: "asd", + full_name: 'Vim repo', + visibility_level: Gitlab::VisibilityLevel::PRIVATE, + clone_url: 'ssh://git@bitbucket.org/asd/vim.git', + has_wiki?: false) end + let(:namespace){ create(:group, owner: user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } @@ -22,7 +27,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) - project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params) + project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params) project = project_creator.execute expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git") diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index bfc6818ac08a8ab510aacd49251ef337987f79a8..a2d84977f585780617d02c101d7d9fbd39f1ed57 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,7 +5,9 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do - subject { described_class.new(project, user, params).execute } + subject do + described_class.new(project, user, params).execute + end context 'when no command is available' do let(:params) { { text: 'issue show 1' } } @@ -52,6 +54,30 @@ describe Gitlab::ChatCommands::Command, service: true do end end + context 'searching for an issue' do + let(:params) { { text: 'issue search find me' } } + let!(:issue) { create(:issue, project: project, title: 'find me') } + + before do + project.team << [user, :master] + end + + context 'a single issue is found' do + it 'presents the issue' do + expect(subject[:text]).to match(issue.title) + end + end + + context 'multiple issues found' do + let!(:issue2) { create(:issue, project: project, title: "someone find me") } + + it 'shows a link to the new issue' do + expect(subject[:text]).to match(issue.title) + expect(subject[:text]).to match(issue2.title) + end + end + end + context 'when trying to do deployment' do let(:params) { { text: 'deploy staging to production' } } let!(:build) { create(:ci_build, project: project) } @@ -74,7 +100,7 @@ describe Gitlab::ChatCommands::Command, service: true do end it 'returns action' do - expect(subject[:text]).to include('Deployment from staging to production started') + expect(subject[:text]).to include('Deployment from staging to production started.') expect(subject[:response_type]).to be(:in_channel) end @@ -91,4 +117,26 @@ describe Gitlab::ChatCommands::Command, service: true do end end end + + describe '#match_command' do + subject { described_class.new(project, user, params).match_command.first } + + context 'IssueShow is triggered' do + let(:params) { { text: 'issue show 123' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) } + end + + context 'IssueCreate is triggered' do + let(:params) { { text: 'issue create my title' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + end + + context 'IssueSearch is triggered' do + let(:params) { { text: 'issue search my query' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) } + end + end end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 39069b49978e5a309a87f7f2d94cc7b110553e07..98effecdbbc8deae5a8dae74a7db2cac29451e63 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -56,7 +56,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do it 'returns an error if the user is not allowed to do forced pushes to protected branches' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false) expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.') @@ -88,8 +87,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end it 'returns an error if the user is not allowed to delete protected branches' do - expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false) - expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to delete protected branches from this project.') end diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f6288011494be13af46e00feeb1c02a379c5220b --- /dev/null +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::Checks::ChangeAccess, lib: true do + let(:project) { create(:project) } + + context "exit code checking" do + it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + + expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error + end + + it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + + expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index d97806295fb32ba14fbf14dd6a450b9140a4bde4..2adbed2154fcd79c390d30bb5fd7f8debf753381 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -196,22 +196,5 @@ describe Gitlab::Ci::Config::Entry::Environment do end end end - - context 'when invalid URL is used' do - let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } } - - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end - - describe '#errors?' do - it 'contains error about invalid URL' do - expect(entry.errors) - .to include "environment url must be a valid url" - end - end - end end end diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b3c07347de132436546eea6672d0784bb4590997 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Cancelable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe '#group' do + it 'does not override status group' do + expect(status).to receive(:group) + + subject.group + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/cancel" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'ban' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Cancel' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is cancelable' do + let(:build) do + create(:ci_build, :running) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not cancelable' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..40b96b1807b096a553dd5028ba30da93d11d9e35 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Common do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject do + Gitlab::Ci::Status::Core + .new(build, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read build' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read build' do + before { project.update(public_builds: false) } + + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the build details page' do + expect(subject.details_path).to include "builds/#{build.id}" + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..dccb29b5ef606b380137e2b9090bf616bbfe4afe --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -0,0 +1,141 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Factory do + let(:user) { create(:user) } + let(:project) { build.project } + + subject { described_class.new(build, user) } + let(:status) { subject.fabricate! } + + before { project.team << [user, :developer] } + + context 'when build is successful' do + let(:build) { create(:ci_build, :success) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'passed' + expect(status.icon).to eq 'icon_status_success' + expect(status.label).to eq 'passed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is failed' do + let(:build) { create(:ci_build, :failed) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is a canceled' do + let(:build) { create(:ci_build, :canceled) } + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'canceled' + expect(status.icon).to eq 'icon_status_canceled' + expect(status.label).to eq 'canceled' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is running' do + let(:build) { create(:ci_build, :running) } + + it 'fabricates a canceable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'running' + expect(status.icon).to eq 'icon_status_running' + expect(status.label).to eq 'running' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is pending' do + let(:build) { create(:ci_build, :pending) } + + it 'fabricates a cancelable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'pending' + expect(status.icon).to eq 'icon_status_pending' + expect(status.label).to eq 'pending' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is skipped' do + let(:build) { create(:ci_build, :skipped) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'skipped' + expect(status.icon).to eq 'icon_status_skipped' + expect(status.label).to eq 'skipped' + expect(status).to have_details + expect(status).not_to have_action + end + end + + context 'when build is a manual action' do + context 'when build is a play action' do + let(:build) { create(:ci_build, :playable) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Play + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual play action' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is an environment stop action' do + let(:build) { create(:ci_build, :playable, :teardown_environment) } + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Stop + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual stop action' + expect(status).to have_details + expect(status).to have_action + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f1b50a59ce673258c3d0ffc21226b0929511dc8b --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Play do + let(:status) { double('core') } + let(:user) { double('user') } + + subject { described_class.new(status) } + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual play action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe '#group' do + it { expect(subject.group).to eq 'manual' } + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'play' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Play' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'does not match' do + expect(subject).to be false + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'is a correct match' do + expect(subject).to be true + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..62036f8ec5d97779de4112b94e9a934106a656f2 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Retryable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe '#group' do + it 'does not override status group' do + expect(status).to receive(:group) + + subject.group + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/retry" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'refresh' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Retry' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is retryable' do + let(:build) do + create(:ci_build, :success) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not retryable' do + let(:build) { create(:ci_build, :running) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..597e02e86e48a9496830660632844e690946f7b7 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Stop do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual stop action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe '#group' do + it { expect(subject.group).to eq 'manual' } + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'stop' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Stop' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'does not match' do + expect(subject).to be false + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 619ecbcba6737475dd5336859ee4c515baf1bf9f..38412fe2e4f67fff2e82ae5d76043707e6bf0359 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Canceled do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'canceled' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Canceled do it { expect(subject.icon).to eq 'icon_status_canceled' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: canceled' } + describe '#group' do + it { expect(subject.group).to eq 'canceled' } end end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 157302c65a845c248fa593038050cfcdf113100f..6d847484693ff4feaec7e8898759c26eb574b12d 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Created do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'created' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Created do it { expect(subject.icon).to eq 'icon_status_created' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: created' } + describe '#group' do + it { expect(subject.group).to eq 'created' } end end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 120e121aae5f31849f1b92e1e52992a04ab3b502..c2d74ca5cde0cbad5026e10fd590829d02b33c9d 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' describe Gitlab::Ci::Status::Extended do subject do - Class.new.extend(described_class) + Class.new.include(described_class) end it 'requires subclass to implement matcher' do - expect { subject.matches?(double) } + expect { subject.matches?(double, double) } .to raise_error(NotImplementedError) end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index d5bd7f7102ba959fb9506572a2caf660650c2031..f92a1c149bf1c604c47fe2f81b917b57d649c395 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -2,15 +2,17 @@ require 'spec_helper' describe Gitlab::Ci::Status::Factory do subject do - described_class.new(object) + described_class.new(resource, user) end + let(:user) { create(:user) } + let(:status) { subject.fabricate! } context 'when object has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do - let(:object) { double(status: core_status) } + let(:resource) { double(status: core_status) } it "fabricates a core status #{core_status}" do expect(status).to be_a( diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 0b3cb8168e6eb1c49d83dc481b62bb73268f0c22..990d686d22cdf91b546706e244b2357e5dbf78be 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Failed do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'failed' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Failed do it { expect(subject.icon).to eq 'icon_status_failed' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: failed' } + describe '#group' do + it { expect(subject.group).to eq 'failed' } end end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 57c901c1202fa3bfb506633813ec654b86c8d148..7bb6579c317c88a8df68efd837dd8d552722bfb3 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pending do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'pending' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Pending do it { expect(subject.icon).to eq 'icon_status_pending' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: pending' } + describe '#group' do + it { expect(subject.group).to eq 'pending' } end end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index 21adee3f8e7d462a5d579ed628239bf4d010c380..d665674bf70095e8ff8ae2726c3b33a198dee2ab 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -1,23 +1,36 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Common do - let(:pipeline) { create(:ci_pipeline) } + let(:user) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } subject do - Class.new(Gitlab::Ci::Status::Core) - .new(pipeline).extend(described_class) + Gitlab::Ci::Status::Core + .new(pipeline, user) + .extend(described_class) end - it 'does not have action' do - expect(subject).not_to have_action + describe '#has_action?' do + it { is_expected.not_to have_action } end - it 'has details' do - expect(subject).to have_details + describe '#has_details?' do + context 'when user has access to read pipeline' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read pipeline' do + it { is_expected.not_to have_details } + end end - it 'links to the pipeline details page' do - expect(subject.details_path) - .to include "pipelines/#{pipeline.id}" + describe '#details_path' do + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end end end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index d6243940f2e5eaf37bc389c268d3b6aba3db811d..d4a2dc7fcc1e1bb0d5213ffb099dc12cd8a61f44 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -1,14 +1,21 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do + let(:user) { create(:user) } + let(:project) { pipeline.project } + subject do - described_class.new(pipeline) + described_class.new(pipeline, user) end let(:status) do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when pipeline has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb index 02e526e3de2001aa6dccb43454dc2eda24b6e892..979160eb9c461d28c5bf8c1034c4e663b928866e 100644 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do it { expect(subject.icon).to eq 'icon_status_warning' } end + describe '#group' do + it { expect(subject.group).to eq 'success_with_warnings' } + end + describe '.matches?' do context 'when pipeline is successful' do let(:pipeline) do @@ -29,13 +33,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'is a correct match' do - expect(described_class.matches?(pipeline)).to eq true + expect(described_class.matches?(pipeline, double)).to eq true end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end @@ -51,13 +55,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do end it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end context 'when pipeline does not have warnings' do it 'does not match' do - expect(described_class.matches?(pipeline)).to eq false + expect(described_class.matches?(pipeline, double)).to eq false end end end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index c023f1872cca14b647dd30017649d1a175466b30..852d6c06baf7db1125ca8e4e547b5b333aca1854 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Running do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'running' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Running do it { expect(subject.icon).to eq 'icon_status_running' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: running' } + describe '#group' do + it { expect(subject.group).to eq 'running' } end end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index d4f7f4b3b707fec1f0bbf9fe9957199b0adf60b8..e00b356a24b6e19711a6a55579a881e91daa24ef 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Skipped do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'skipped' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Skipped do it { expect(subject.icon).to eq 'icon_status_skipped' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: skipped' } + describe '#group' do + it { expect(subject.group).to eq 'skipped' } end end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index f3259c6f23e0738cbba6c14f70a179065b1f0dd5..8814a7614a0715ff95da507f92cc38fee506f0fb 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -1,26 +1,43 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Common do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do Class.new(Gitlab::Ci::Status::Core) - .new(stage).extend(described_class) + .new(stage, user).extend(described_class) end it 'does not have action' do expect(subject).not_to have_action end - it 'has details' do - expect(subject).to have_details - end - it 'links to the pipeline details page' do expect(subject.details_path) .to include "pipelines/#{pipeline.id}" expect(subject.details_path) .to include "##{stage.name}" end + + context 'when user has permission to read pipeline' do + before do + project.team << [user, :master] + end + + it 'has details' do + expect(subject).to have_details + end + end + + context 'when user does not have permission to read pipeline' do + it 'does not have details' do + expect(subject).not_to have_details + end + end end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 17929665c836f11f924ba4dc906df0d04a1697c0..6f8721d30c22c14de01d88ea1095397a6643de73 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -1,17 +1,26 @@ require 'spec_helper' describe Gitlab::Ci::Status::Stage::Factory do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end subject do - described_class.new(stage) + described_class.new(stage, user) end let(:status) do subject.fabricate! end + before do + project.team << [user, :developer] + end + context 'when stage has a core status' do HasStatus::AVAILABLE_STATUSES.each do |core_status| context "when core status is #{core_status}" do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 9e261a3aa5f207ac63acb708610093449f991524..4a89e1faf4043e4bdd26cd1456a8876902237cfd 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Status::Success do - subject { described_class.new(double('subject')) } + subject do + described_class.new(double('subject'), double('user')) + end describe '#text' do it { expect(subject.label).to eq 'passed' } @@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Success do it { expect(subject.icon).to eq 'icon_status_success' } end - describe '#title' do - it { expect(subject.title).to eq 'Double: passed' } + describe '#group' do + it { expect(subject.group).to eq 'success' } end end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0267e8c2f69e13593ea1b6803239c1be7686a047 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::CodeEventFetcher do + let(:stage_name) { :code } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb deleted file mode 100644 index 43f42d1bde878d4007746850ffe3deb05e2ba9f0..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::CodeEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e8fc67acf0549463260eabff39146af11735b8f7 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::CodeStage do + let(:stage_name) { :code } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 6062e7af4f5712c1bff87d1ad4c45b8af2349201..9d2ba4819198f62c9c1c2ee2a3d1108a34ea96eb 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' -describe Gitlab::CycleAnalytics::Events do +describe 'cycle analytics events' do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - subject { described_class.new(project: project, options: { from: from_date, current_user: user }) } + let(:events) do + CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events + end before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) @@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do end describe '#issue_events' do + let(:stage) { :issue } + it 'has the total time' do - expect(subject.issue_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.issue_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.issue_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.issue_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.issue_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.issue_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end describe '#plan_events' do + let(:stage) { :plan } + it 'has a title' do - expect(subject.plan_events.first[:title]).not_to be_nil + expect(events.first[:title]).not_to be_nil end it 'has a sha short ID' do - expect(subject.plan_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the URL' do - expect(subject.plan_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the total time' do - expect(subject.plan_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.plan_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.plan_events.first[:author][:name]).not_to be_nil + expect(events.first[:author][:name]).not_to be_nil end end describe '#code_events' do + let(:stage) { :code } + before do create_commit_referencing_issue(context) end it 'has the total time' do - expect(subject.code_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.code_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.code_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.code_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.code_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#test_events' do + let(:stage) { :test } + let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.test_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.test_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.test_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.test_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.test_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.test_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.test_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.test_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.test_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end end describe '#review_events' do + let(:stage) { :review } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } it 'has the total time' do - expect(subject.review_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.review_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.review_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has the URL' do - expect(subject.review_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has a state' do - expect(subject.review_events.first[:state]).not_to be_nil + expect(events.first[:state]).not_to be_nil end it 'has a created_at timestamp' do - expect(subject.review_events.first[:created_at]).not_to be_nil + expect(events.first[:created_at]).not_to be_nil end it "has the author's URL" do - expect(subject.review_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#staging_events' do + let(:stage) { :staging } let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.staging_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.staging_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.staging_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.staging_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.staging_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.staging_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.staging_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.staging_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.staging_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.staging_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#production_events' do + let(:stage) { :production } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } before do @@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the total time' do - expect(subject.production_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.production_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.production_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.production_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.production_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.production_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.production_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd9fa2fee497b349e28ddeb73414e2c7c0bfe9c2 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::IssueEventFetcher do + let(:stage_name) { :issue } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb deleted file mode 100644 index 1c5c308da7dfa3b6f1a0481de7430e4db14e5c46..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::IssueEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3127f01989dd926add165bed95cc207e77d0c8cf --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::IssueStage do + let(:stage_name) { :issue } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb similarity index 53% rename from spec/lib/gitlab/cycle_analytics/plan_event_spec.rb rename to spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb index 4a5604115ec91614d60830a21555710e4b173940..2e5dc5b55476ebcd5a6b697b68ff2e490d8cabdf 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -1,15 +1,13 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' -describe Gitlab::CycleAnalytics::PlanEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end +describe Gitlab::CycleAnalytics::PlanEventFetcher do + let(:stage_name) { :plan } + it_behaves_like 'default query config' do context 'no commits' do it 'does not blow up if there are no commits' do - allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}]) + allow(event).to receive(:event_result).and_return([{}]) expect { event.fetch }.not_to raise_error end diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4c715921ad662365b93ef4f0fd9875b56c08d87c --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::PlanStage do + let(:stage_name) { :plan } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7400118130533e072c59f71b8a71c11971bc961d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ProductionEventFetcher do + let(:stage_name) { :production } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb deleted file mode 100644 index ac17e3b42872bc30c89af1e5eadc12752750cf17..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ProductionEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..916684b81ebb11d62adce1712e24a8c2f7be3313 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ProductionStage do + let(:stage_name) { :production } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f67c95ed4cb9e359b5ad697076917727ccefb67 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ReviewEventFetcher do + let(:stage_name) { :review } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb deleted file mode 100644 index 1ff53aa022771d76bee22a7027a516bfb5a89c02..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ReviewEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1412c8dfa082aafe9711cbf545d189ca164b2d30 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ReviewStage do + let(:stage_name) { :review } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 7019e4c3351330d7d01ad41405891e6e03a93885..9c5e57342e98f42b3a2d301c22bcf1505da2be39 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -1,20 +1,13 @@ require 'spec_helper' shared_examples 'default query config' do - let(:event) { described_class.new(project: double, options: {}) } - - it 'has the start attributes' do - expect(event.start_time_attrs).not_to be_nil - end + let(:project) { create(:empty_project) } + let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) } it 'has the stage attribute' do expect(event.stage).not_to be_nil end - it 'has the end attributes' do - expect(event.end_time_attrs).not_to be_nil - end - it 'has the projection attributes' do expect(event.projections).not_to be_nil end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..08425acbfc811f31318e8821234b9c7ffd06af17 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'base stage' do + let(:stage) { described_class.new(project: double, options: {}) } + + before do + allow(stage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'has the median data value' do + expect(stage.as_json[:value]).not_to be_nil + end + + it 'has the median data stage' do + expect(stage.as_json[:title]).not_to be_nil + end + + it 'has the median data description' do + expect(stage.as_json[:description]).not_to be_nil + end + + it 'has the title' do + expect(stage.title).to eq(stage_name.to_s.capitalize) + end + + it 'has the events' do + expect(stage.events).not_to be_nil + end +end diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb similarity index 76% rename from spec/models/cycle_analytics/summary_spec.rb rename to spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 725bc68b25f457f1b9b9116d5d5b6e329756d393..fb6b6c4a8d282090148156b5d21970fef4235fa7 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' -describe CycleAnalytics::Summary, models: true do +describe Gitlab::CycleAnalytics::StageSummary, models: true do let(:project) { create(:project) } - let(:from) { Time.now } + let(:from) { 1.day.ago } let(:user) { create(:user, :admin) } - subject { described_class.new(project, user, from: from) } + subject { described_class.new(project, from: Time.now, current_user: user).data } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do Timecop.freeze(5.days.ago) { create(:issue, project: project) } Timecop.freeze(5.days.from_now) { create(:issue, project: project) } - expect(subject.new_issues).to eq(1) + expect(subject.first[:value]).to eq(1) end it "doesn't find issues from other projects" do Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } - expect(subject.new_issues).to eq(0) + expect(subject.first[:value]).to eq(0) end end @@ -26,19 +26,19 @@ describe CycleAnalytics::Summary, models: true do Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } - expect(subject.commits).to eq(1) + expect(subject.second[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } - expect(subject.commits).to eq(0) + expect(subject.second[:value]).to eq(0) end it "finds a large (> 100) snumber of commits if present" do Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } - expect(subject.commits).to eq(100) + expect(subject.second[:value]).to eq(100) end end @@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do Timecop.freeze(5.days.ago) { create(:deployment, project: project) } Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } - expect(subject.deploys).to eq(1) + expect(subject.third[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } - expect(subject.deploys).to eq(0) + expect(subject.third[:value]).to eq(0) end end end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bbc82496340238e20b0510166b69a5a71e70aa2a --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::StagingEventFetcher do + let(:stage_name) { :staging } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb deleted file mode 100644 index 4862d4765f2151cbc117c25d5f8d722901899f9d..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::StagingEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8154b3ac701847dfd5c83a18c5729aeac36463e2 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::StagingStage do + let(:stage_name) { :staging } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6639fa54e0ebeac1d62761cb97ad2f078610cfac --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::TestEventFetcher do + let(:stage_name) { :test } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb deleted file mode 100644 index e249db69fc6832d68e1c4e807141b088fc249bc7..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::TestEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eacde22cd563330a97b7c963b202b07d50ed6cfc --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::TestStage do + let(:stage_name) { :test } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 2a680f03476acbeeacee02ef9f5c63db323cfd28..f2bc15d39d7a19e8016ce6607d461da36600c3cd 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -1,21 +1,30 @@ require 'spec_helper' describe Gitlab::Diff::FileCollection::MergeRequestDiff do - let(:merge_request) { create :merge_request } + let(:merge_request) { create(:merge_request) } + let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files } - it 'does not hightlight binary files' do + it 'does not highlight binary files' do allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false)) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) - described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files + diff_files end - it 'does not hightlight file if blob is not accessable' do + it 'does not highlight file if blob is not accessable' do allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) - described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files + diff_files + end + + it 'does not files marked as undiffable in .gitattributes' do + allow_any_instance_of(Repository).to receive(:diffable?).and_return(false) + + expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) + + diff_files end end diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index c7a0139d32a1af48ee053446af8c2e794d9cbeba..28698e89c338cee596d73e3cb9ddebc370c9b3cd 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -88,8 +88,6 @@ describe Gitlab::Email::ReplyParser, lib: true do expect(test_parse_body(fixture_file("emails/inline_reply.eml"))). to eq( <<-BODY.strip_heredoc.chomp - On Wed, Oct 8, 2014 at 11:12 AM, techAPJ <info@unconfigured.discourse.org> wrote: - > techAPJ <https://meta.discourse.org/users/techapj> > November 28 > diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index d619e401897ebda7f8e7c12b637ea6e64aac2616..f4703dc704f565ca1ca3c09db00d010bbce58328 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with ignored elements' do let(:text) do "Hi. This references #1, but not `#2`\n" + - '<pre>and not !1</pre>' + '<pre>and not !1</pre>' end it { is_expected.to include issue_first.to_reference(new_project) } diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9c011e34c113f53f4f39c25f76106c7f45762a87 --- /dev/null +++ b/spec/lib/gitlab/git/attributes_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe Gitlab::Git::Attributes, seed_helper: true do + let(:path) do + File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git') + end + + subject { described_class.new(path) } + + describe '#attributes' do + context 'using a path with attributes' do + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.txt')).to eq({ 'text' => true }) + end + + it 'returns a Hash containing multiple attributes' do + expect(subject.attributes('test.sh')). + to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' }) + end + + it 'returns a Hash containing attributes for a file with multiple extensions' do + expect(subject.attributes('test.haml.html')). + to eq({ 'gitlab-language' => 'haml' }) + end + + it 'returns a Hash containing attributes for a file in a directory' do + expect(subject.attributes('foo/bar.txt')).to eq({ 'foo' => true }) + end + + it 'returns a Hash containing attributes with query string parameters' do + expect(subject.attributes('foo.cgi')). + to eq({ 'key' => 'value?p1=v1&p2=v2' }) + end + + it 'returns a Hash containing the attributes for an absolute path' do + expect(subject.attributes('/test.txt')).to eq({ 'text' => true }) + end + + it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do + # When a path is given without a leading slash it should still match + # patterns defined with a leading slash. + expect(subject.attributes('foo.png')). + to eq({ 'gitlab-language' => 'png' }) + + expect(subject.attributes('/foo.png')). + to eq({ 'gitlab-language' => 'png' }) + end + + it 'returns an empty Hash for a defined path without attributes' do + expect(subject.attributes('bla/bla.txt')).to eq({}) + end + + context 'when the "binary" option is set for a path' do + it 'returns true for the "binary" option' do + expect(subject.attributes('test.binary')['binary']).to eq(true) + end + + it 'returns false for the "diff" option' do + expect(subject.attributes('test.binary')['diff']).to eq(false) + end + end + end + + context 'using a path without any attributes' do + it 'returns an empty Hash' do + expect(subject.attributes('test.foo')).to eq({}) + end + end + end + + describe '#patterns' do + it 'parses a file with entries' do + expect(subject.patterns).to be_an_instance_of(Hash) + end + + it 'parses an entry that uses a tab to separate the pattern and attributes' do + expect(subject.patterns[File.join(path, '*.md')]). + to eq({ 'gitlab-language' => 'markdown' }) + end + + it 'stores patterns in reverse order' do + first = subject.patterns.to_a[0] + + expect(first[0]).to eq(File.join(path, 'bla/bla.txt')) + end + + # It's a bit hard to test for something _not_ being processed. As such we'll + # just test the number of entries. + it 'ignores any comments and empty lines' do + expect(subject.patterns.length).to eq(10) + end + + it 'does not parse anything when the attributes file does not exist' do + expect(File).to receive(:exist?). + with(File.join(path, 'info/attributes')). + and_return(false) + + expect(subject.patterns).to eq({}) + end + end + + describe '#parse_attributes' do + it 'parses a boolean attribute' do + expect(subject.parse_attributes('text')).to eq({ 'text' => true }) + end + + it 'parses a negated boolean attribute' do + expect(subject.parse_attributes('-text')).to eq({ 'text' => false }) + end + + it 'parses a key-value pair' do + expect(subject.parse_attributes('foo=bar')).to eq({ 'foo' => 'bar' }) + end + + it 'parses multiple attributes' do + input = 'boolean key=value -negated' + + expect(subject.parse_attributes(input)). + to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false }) + end + + it 'parses attributes with query string parameters' do + expect(subject.parse_attributes('foo=bar?baz=1')). + to eq({ 'foo' => 'bar?baz=1' }) + end + end + + describe '#each_line' do + it 'iterates over every line in the attributes file' do + args = [String] * 14 # the number of lines in the file + + expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) + end + + it 'does not yield when the attributes file does not exist' do + expect(File).to receive(:exist?). + with(File.join(path, 'info/attributes')). + and_return(false) + + expect { |b| subject.each_line(&b) }.not_to yield_control + end + + it 'does not yield when the attributes file has an unsupported encoding' do + path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git') + attrs = described_class.new(path) + + expect { |b| attrs.each_line(&b) }.not_to yield_control + end + end +end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e169f5af6b6313ab03eb086a2783a298afa336b4 --- /dev/null +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -0,0 +1,66 @@ +# coding: utf-8 +require "spec_helper" + +describe Gitlab::Git::Blame, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") + end + + context "each count" do + it do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end + + expect(data.size).to eq(95) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("# Contribute to GitLab") + end + end + + context "ISO-8859 encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + end + + it 'converts to UTF-8' do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end + + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("Ä ü") + end + end + + context "unknown encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + end + + it 'converts to UTF-8' do + expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil) + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end + + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq(" ") + end + end +end diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..79b1311ac916fdbb3d074e48dd2807719e213639 --- /dev/null +++ b/spec/lib/gitlab/git/blob_snippet_spec.rb @@ -0,0 +1,19 @@ +# encoding: UTF-8 + +require "spec_helper" + +describe Gitlab::Git::BlobSnippet, seed_helper: true do + describe :data do + context 'empty lines' do + let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) } + + it { expect(snippet.data).to be_nil } + end + + context 'present lines' do + let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') } + + it { expect(snippet.data).to eq("wow\nmuch") } + end + end +end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..84f79ec2391bba4bd36514efc57577920b1eb61c --- /dev/null +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -0,0 +1,489 @@ +# encoding: utf-8 + +require "spec_helper" + +describe Gitlab::Git::Blob, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + describe :initialize do + let(:blob) { Gitlab::Git::Blob.new(name: 'test') } + + it 'handles nil data' do + expect(blob.name).to eq('test') + expect(blob.size).to eq(nil) + expect(blob.loaded_size).to eq(nil) + end + end + + describe :find do + context 'file in subdir' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } + + it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } + it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } + it { expect(blob.path).to eq("files/ruby/popen.rb") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } + it { expect(blob.size).to eq(669) } + it { expect(blob.mode).to eq("100644") } + end + + context 'file in root' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") } + + it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") } + it { expect(blob.name).to eq(".gitignore") } + it { expect(blob.path).to eq(".gitignore") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") } + it { expect(blob.size).to eq(241) } + it { expect(blob.mode).to eq("100644") } + it { expect(blob).not_to be_binary } + end + + context 'file in root with leading slash' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") } + + it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") } + it { expect(blob.name).to eq(".gitignore") } + it { expect(blob.path).to eq(".gitignore") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") } + it { expect(blob.size).to eq(241) } + it { expect(blob.mode).to eq("100644") } + end + + context 'non-exist file' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") } + + it { expect(blob).to be_nil } + end + + context 'six submodule' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') } + + it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } + it { expect(blob.data).to eq('') } + + it 'does not get messed up by load_all_data!' do + blob.load_all_data!(repository) + expect(blob.data).to eq('') + end + + it 'does not mark the blob as binary' do + expect(blob).not_to be_binary + end + end + + context 'large file' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') } + let(:blob_size) { 111803 } + + it { expect(blob.size).to eq(blob_size) } + it { expect(blob.data.length).to eq(blob_size) } + + it 'check that this test is sane' do + expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + end + + it 'can load all data' do + blob.load_all_data!(repository) + expect(blob.data.length).to eq(blob_size) + end + + it 'marks the blob as binary' do + expect(Gitlab::Git::Blob).to receive(:new). + with(hash_including(binary: true)). + and_call_original + + expect(blob).to be_binary + end + end + end + + describe :raw do + let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } + it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } + it { expect(raw_blob.data[0..10]).to eq("require \'fi") } + it { expect(raw_blob.size).to eq(669) } + it { expect(raw_blob.truncated?).to be_falsey } + + context 'large file' do + it 'limits the size of a large file' do + blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1 + buffer = Array.new(blob_size, 0) + rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join('')) + blob = Gitlab::Git::Blob.raw(repository, rugged_blob) + + expect(blob.size).to eq(blob_size) + expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) + expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) + expect(blob.truncated?).to be_truthy + + blob.load_all_data!(repository) + expect(blob.loaded_size).to eq(blob_size) + end + end + end + + describe 'encoding' do + context 'file with russian text' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } + + it { expect(blob.name).to eq("russian.rb") } + it { expect(blob.data.lines.first).to eq("Хороший файл") } + it { expect(blob.size).to eq(23) } + it { expect(blob.truncated?).to be_falsey } + # Run it twice since data is encoded after the first run + it { expect(blob.truncated?).to be_falsey } + it { expect(blob.mode).to eq("100755") } + end + + context 'file with Chinese text' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") } + + it { expect(blob.name).to eq("テスト.txt") } + it { expect(blob.data).to include("ã“ã‚Œã¯ãƒ†ã‚¹ãƒˆ") } + it { expect(blob.size).to eq(340) } + it { expect(blob.mode).to eq("100755") } + it { expect(blob.truncated?).to be_falsey } + end + + context 'file with ISO-8859 text' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") } + + it { expect(blob.name).to eq("iso8859.txt") } + it { expect(blob.loaded_size).to eq(4) } + it { expect(blob.size).to eq(4) } + it { expect(blob.mode).to eq("100644") } + it { expect(blob.truncated?).to be_falsey } + end + end + + describe 'mode' do + context 'file regular' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/ruby/regex.rb' + ) + end + + it { expect(blob.name).to eq('regex.rb') } + it { expect(blob.path).to eq('files/ruby/regex.rb') } + it { expect(blob.size).to eq(1200) } + it { expect(blob.mode).to eq("100644") } + end + + context 'file binary' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/executables/ls' + ) + end + + it { expect(blob.name).to eq('ls') } + it { expect(blob.path).to eq('files/executables/ls') } + it { expect(blob.size).to eq(110080) } + it { expect(blob.mode).to eq("100755") } + end + + context 'file symlink to regular' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/links/ruby-style-guide.md' + ) + end + + it { expect(blob.name).to eq('ruby-style-guide.md') } + it { expect(blob.path).to eq('files/links/ruby-style-guide.md') } + it { expect(blob.size).to eq(31) } + it { expect(blob.mode).to eq("120000") } + end + + context 'file symlink to binary' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/links/touch' + ) + end + + it { expect(blob.name).to eq('touch') } + it { expect(blob.path).to eq('files/links/touch') } + it { expect(blob.size).to eq(20) } + it { expect(blob.mode).to eq("120000") } + end + end + + describe :commit do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + let(:commit_options) do + { + file: { + content: 'Lorem ipsum...', + path: 'documents/story.txt' + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Wow such commit', + branch: 'fix-mode' + } + } + end + + let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) } + let(:commit) { repository.lookup(commit_sha) } + + it 'should add file with commit' do + # Commit message valid + expect(commit.message).to eq('Wow such commit') + + tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' } + + # Directory was created + expect(tree[:type]).to eq(:tree) + + # File was created + expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt') + end + + describe "ref updating" do + it 'creates a commit but does not udate a ref' do + commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false} + commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts) + commit = repository.lookup(commit_sha) + + # Commit message valid + expect(commit.message).to eq('Wow such commit') + + # Does not update any related ref + expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid) + expect(repository.lookup("HEAD").oid).not_to eq(commit.oid) + end + end + + describe 'reject updates' do + it 'should reject updates' do + commit_options[:file][:update] = false + commit_options[:file][:path] = 'files/executables/ls' + + expect{ commit_sha }.to raise_error('Filename already exists; update not allowed') + end + end + + describe 'file modes' do + it 'should preserve file modes with commit' do + commit_options[:file][:path] = 'files/executables/ls' + + entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path]) + expect(entry[:filemode]).to eq(0100755) + end + end + end + + describe :rename do + let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) } + let(:rename_options) do + { + file: { + path: 'NEWCONTRIBUTING.md', + previous_path: 'CONTRIBUTING.md', + content: 'Lorem ipsum...', + update: true + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Rename readme', + branch: 'master' + } + } + end + + let(:rename_options2) do + { + file: { + content: 'Lorem ipsum...', + path: 'bin/new_executable', + previous_path: 'bin/executable', + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Updates toberenamed.txt', + branch: 'master', + update_ref: false + } + } + end + + it 'maintains file permissions when renaming' do + mode = 0o100755 + + Gitlab::Git::Blob.rename(repository, rename_options2) + + expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode) + end + + it 'renames the file with commit and not change file permissions' do + ref = rename_options[:commit][:branch] + + expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil + expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1) + + expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil + expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil + end + end + + describe :remove do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + let(:commit_options) do + { + file: { + path: 'README.md' + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Remove readme', + branch: 'feature' + } + } + end + + let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) } + let(:commit) { repository.lookup(commit_sha) } + let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") } + + it 'should remove file with commit' do + # Commit message valid + expect(commit.message).to eq('Remove readme') + + # File was removed + expect(blob).to be_nil + end + end + + describe :lfs_pointers do + context 'file a valid lfs pointer' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/image.jpg' + ) + end + + it { expect(blob.lfs_pointer?).to eq(true) } + it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") } + it { expect(blob.lfs_size).to eq("19548") } + it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") } + it { expect(blob.name).to eq("image.jpg") } + it { expect(blob.path).to eq("files/lfs/image.jpg") } + it { expect(blob.size).to eq(130) } + it { expect(blob.mode).to eq("100644") } + end + + describe 'file an invalid lfs pointer' do + context 'with correct version header but incorrect size and oid' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/archive-invalid.tar' + ) + end + + it { expect(blob.lfs_pointer?).to eq(false) } + it { expect(blob.lfs_oid).to eq(nil) } + it { expect(blob.lfs_size).to eq(nil) } + it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") } + it { expect(blob.name).to eq("archive-invalid.tar") } + it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") } + it { expect(blob.size).to eq(43) } + it { expect(blob.mode).to eq("100644") } + end + + context 'with correct version header and size but incorrect size and oid' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/picture-invalid.png' + ) + end + + it { expect(blob.lfs_pointer?).to eq(false) } + it { expect(blob.lfs_oid).to eq(nil) } + it { expect(blob.lfs_size).to eq("1575078") } + it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") } + it { expect(blob.name).to eq("picture-invalid.png") } + it { expect(blob.path).to eq("files/lfs/picture-invalid.png") } + it { expect(blob.size).to eq(57) } + it { expect(blob.mode).to eq("100644") } + end + + context 'with correct version header and size but invalid size and oid' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/file-invalid.zip' + ) + end + + it { expect(blob.lfs_pointer?).to eq(false) } + it { expect(blob.lfs_oid).to eq(nil) } + it { expect(blob.lfs_size).to eq(nil) } + it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") } + it { expect(blob.name).to eq("file-invalid.zip") } + it { expect(blob.path).to eq("files/lfs/file-invalid.zip") } + it { expect(blob.size).to eq(60) } + it { expect(blob.mode).to eq("100644") } + end + end + end +end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..78234b396c5a66834cd2b73e5e0b6ac7bbdd9f21 --- /dev/null +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -0,0 +1,31 @@ +require "spec_helper" + +describe Gitlab::Git::Branch, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + subject { repository.branches } + + it { is_expected.to be_kind_of Array } + + describe '#size' do + subject { super().size } + it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } + end + + describe 'first branch' do + let(:branch) { repository.branches.first } + + it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) } + it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } + end + + describe 'master branch' do + let(:branch) do + repository.branches.find { |branch| branch.name == 'master' } + end + + it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) } + end + + it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } +end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e1be6784c202a3429676cfef641275959d512d5b --- /dev/null +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -0,0 +1,408 @@ +require "spec_helper" + +describe Gitlab::Git::Commit, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } + let(:rugged_commit) do + repository.rugged.lookup(SeedRepo::Commit::ID) + end + + describe "Commit info" do + before do + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + + @committer = { + email: 'mike@smith.com', + name: "Mike Smith", + time: Time.now + } + + @author = { + email: 'john@smith.com', + name: "John Smith", + time: Time.now + } + + @parents = [repo.head.target] + @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) } + @tree = @parents.first.tree + + sha = Rugged::Commit.create( + repo, + author: @author, + committer: @committer, + tree: @tree, + parents: @parents, + message: "Refactoring specs", + update_ref: "HEAD" + ) + + @raw_commit = repo.lookup(sha) + @commit = Gitlab::Git::Commit.new(@raw_commit) + end + + it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) } + it { expect(@commit.id).to eq(@raw_commit.oid) } + it { expect(@commit.sha).to eq(@raw_commit.oid) } + it { expect(@commit.safe_message).to eq(@raw_commit.message) } + it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) } + it { expect(@commit.date).to eq(@raw_commit.committer[:time]) } + it { expect(@commit.author_email).to eq(@author[:email]) } + it { expect(@commit.author_name).to eq(@author[:name]) } + it { expect(@commit.committer_name).to eq(@committer[:name]) } + it { expect(@commit.committer_email).to eq(@committer[:email]) } + it { expect(@commit.different_committer?).to be_truthy } + it { expect(@commit.parents).to eq(@gitlab_parents) } + it { expect(@commit.parent_id).to eq(@parents.first.oid) } + it { expect(@commit.no_commit_message).to eq("--no commit message") } + it { expect(@commit.tree).to eq(@tree) } + + after do + # Erase the new commit so other tests get the original repo + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + end + end + + context 'Class methods' do + describe :find do + it "should return first head commit if without params" do + expect(Gitlab::Git::Commit.last(repository).id).to eq( + repository.raw.head.target.oid + ) + end + + it "should return valid commit" do + expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit + end + + it "should return valid commit for tag" do + expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + end + + it "should return nil for non-commit ids" do + blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") + expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil + end + + it "should return nil for parent of non-commit object" do + blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") + expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil + end + + it "should return nil for nonexisting ids" do + expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil + end + + context 'with broken repo' do + let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) } + + it 'returns nil' do + expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil + end + end + end + + describe :last_for_path do + context 'no path' do + subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::LastCommit::ID) } + end + end + + context 'path' do + subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::Commit::ID) } + end + end + + context 'ref + path' do + subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::BigCommit::ID) } + end + end + end + + describe "where" do + context 'path is empty string' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'master', + path: '', + limit: 10 + ) + + commits.map { |c| c.id } + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + end + + context 'path is nil' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'master', + path: nil, + limit: 10 + ) + + commits.map { |c| c.id } + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + end + + context 'ref is branch name' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'master', + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") } + it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") } + end + + context 'ref is commit id' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e", + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") } + it { is_expected.not_to include(SeedRepo::Commit::ID) } + end + + context 'ref is tag' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'v1.0.0', + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") } + it { is_expected.not_to include(SeedRepo::Commit::ID) } + end + end + + describe :between do + subject do + commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) + commits.map { |c| c.id } + end + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + it { is_expected.to include(SeedRepo::Commit::ID) } + it { is_expected.not_to include(SeedRepo::FirstCommit::ID) } + end + + describe :find_all do + context 'max_count' do + subject do + commits = Gitlab::Git::Commit.find_all( + repository, + max_count: 50 + ) + + commits.map { |c| c.id } + end + + it 'has 31 elements' do + expect(subject.size).to eq(33) + end + it { is_expected.to include(SeedRepo::Commit::ID) } + it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.to include(SeedRepo::FirstCommit::ID) } + end + + context 'ref + max_count + skip' do + subject do + commits = Gitlab::Git::Commit.find_all( + repository, + ref: 'master', + max_count: 50, + skip: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 23 elements' do + expect(subject.size).to eq(24) + end + it { is_expected.to include(SeedRepo::Commit::ID) } + it { is_expected.to include(SeedRepo::FirstCommit::ID) } + it { is_expected.not_to include(SeedRepo::LastCommit::ID) } + end + + context 'contains feature + max_count' do + subject do + commits = Gitlab::Git::Commit.find_all( + repository, + contains: 'feature', + max_count: 7 + ) + + commits.map { |c| c.id } + end + + it 'has 7 elements' do + expect(subject.size).to eq(7) + end + + it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.not_to include(SeedRepo::Commit::ID) } + it { is_expected.to include(SeedRepo::BigCommit::ID) } + end + end + end + + describe :init_from_rugged do + let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) } + subject { gitlab_commit } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::Commit::ID) } + end + end + + describe :init_from_hash do + let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } + subject { commit } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(sample_commit_hash[:id])} + end + + describe '#message' do + subject { super().message } + it { is_expected.to eq(sample_commit_hash[:message])} + end + end + + describe :stats do + subject { commit.stats } + + describe '#additions' do + subject { super().additions } + it { is_expected.to eq(11) } + end + + describe '#deletions' do + subject { super().deletions } + it { is_expected.to eq(6) } + end + end + + describe :to_diff do + subject { commit.to_diff } + + it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" } + it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} + end + + describe :has_zero_stats? do + it { expect(commit.has_zero_stats?).to eq(false) } + end + + describe :to_patch do + subject { commit.to_patch } + + it { is_expected.to include "From #{SeedRepo::Commit::ID}" } + it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} + end + + describe :to_hash do + let(:hash) { commit.to_hash } + subject { hash } + + it { is_expected.to be_kind_of Hash } + + describe '#keys' do + subject { super().keys.sort } + it { is_expected.to match(sample_commit_hash.keys.sort) } + end + end + + describe :diffs do + subject { commit.diffs } + + it { is_expected.to be_kind_of Gitlab::Git::DiffCollection } + it { expect(subject.count).to eq(2) } + it { expect(subject.first).to be_kind_of Gitlab::Git::Diff } + end + + describe :ref_names do + let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } + subject { commit.ref_names(repository) } + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + it { is_expected.to include("master") } + it { is_expected.not_to include("feature") } + end + + def sample_commit_hash + { + author_email: "dmitriy.zaporozhets@gmail.com", + author_name: "Dmitriy Zaporozhets", + authored_date: "2012-02-27 20:51:12 +0200", + committed_date: "2012-02-27 20:51:12 +0200", + committer_email: "dmitriy.zaporozhets@gmail.com", + committer_name: "Dmitriy Zaporozhets", + id: SeedRepo::Commit::ID, + message: "tree css fixes", + parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"] + } + end +end diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f66b68e42186354b00a94dae10ac1fa9e6c145b3 --- /dev/null +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -0,0 +1,109 @@ +require "spec_helper" + +describe Gitlab::Git::Compare, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } + let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } + + describe :commits do + subject do + compare.commits.map(&:id) + end + + it 'has 8 elements' do + expect(subject.size).to eq(8) + end + + it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) } + + context 'non-existing base ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) } + + it { is_expected.to be_empty } + end + + context 'non-existing head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') } + + it { is_expected.to be_empty } + end + + context 'base ref is equal to head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) } + + it { is_expected.to be_empty } + end + + context 'providing nil as base ref or head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) } + + it { is_expected.to be_empty } + end + end + + describe :diffs do + subject do + compare.diffs.map(&:new_path) + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + + it { is_expected.to include('files/ruby/popen.rb') } + it { is_expected.not_to include('LICENSE') } + + context 'non-existing base ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) } + + it { is_expected.to be_empty } + end + + context 'non-existing head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') } + + it { is_expected.to be_empty } + end + end + + describe :same do + subject do + compare.same + end + + it { is_expected.to eq(false) } + + context 'base ref is equal to head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) } + + it { is_expected.to eq(true) } + end + end + + describe :commits_straight do + subject do + compare_straight.commits.map(&:id) + end + + it 'has 8 elements' do + expect(subject.size).to eq(8) + end + + it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) } + end + + describe :diffs_straight do + subject do + compare_straight.diffs.map(&:new_path) + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + + it { is_expected.to include('files/ruby/popen.rb') } + it { is_expected.not_to include('LICENSE') } + end +end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4fa72c565aece98a15cd5368521bf9c10acae903 --- /dev/null +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -0,0 +1,460 @@ +require 'spec_helper' + +describe Gitlab::Git::DiffCollection, seed_helper: true do + subject do + Gitlab::Git::DiffCollection.new( + iterator, + max_files: max_files, + max_lines: max_lines, + all_diffs: all_diffs, + no_collapse: no_collapse + ) + end + let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) } + let(:file_count) { 0 } + let(:line_length) { 1 } + let(:line_count) { 1 } + let(:max_files) { 10 } + let(:max_lines) { 100 } + let(:all_diffs) { false } + let(:no_collapse) { true } + + describe '#to_a' do + subject { super().to_a } + it { is_expected.to be_kind_of ::Array } + end + + describe :decorate! do + let(:file_count) { 3 } + + it 'modifies the array in place' do + count = 0 + subject.decorate! { |d| !d.nil? && count += 1 } + expect(subject.to_a).to eq([1, 2, 3]) + expect(count).to eq(3) + end + + it 'avoids future iterator iterations' do + subject.decorate! { |d| d unless d.nil? } + + expect(iterator).not_to receive(:each) + + subject.overflow? + end + end + + context 'overflow handling' do + context 'adding few enough files' do + let(:file_count) { 3 } + + context 'and few enough lines' do + let(:line_count) { 10 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3') } + end + it { expect(subject.size).to eq(3) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3') } + end + it { expect(subject.size).to eq(3) } + end + end + + context 'and too many lines' do + let(:line_count) { 1000 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('0+') } + end + it { expect(subject.size).to eq(0) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3') } + end + it { expect(subject.size).to eq(3) } + end + end + end + + context 'adding too many files' do + let(:file_count) { 11 } + + context 'and few enough lines' do + let(:line_count) { 1 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('10+') } + end + it { expect(subject.size).to eq(10) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('11') } + end + it { expect(subject.size).to eq(11) } + end + end + + context 'and too many lines' do + let(:line_count) { 30 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3+') } + end + it { expect(subject.size).to eq(3) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('11') } + end + it { expect(subject.size).to eq(11) } + end + end + end + + context 'adding exactly the maximum number of files' do + let(:file_count) { 10 } + + context 'and few enough lines' do + let(:line_count) { 1 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('10') } + end + it { expect(subject.size).to eq(10) } + end + end + + context 'adding too many bytes' do + let(:file_count) { 10 } + let(:line_length) { 5200 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('9+') } + end + it { expect(subject.size).to eq(9) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('10') } + end + it { expect(subject.size).to eq(10) } + end + end + end + + describe 'empty collection' do + subject { Gitlab::Git::DiffCollection.new([]) } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_truthy } + end + + describe '#size' do + subject { super().size } + it { is_expected.to eq(0) } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('0')} + end + end + + describe :each do + context 'when diff are too large' do + let(:collection) do + Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }]) + end + + it 'yields Diff instances even when they are too large' do + expect { |b| collection.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'prunes diffs that are too large' do + diff = nil + + collection.each do |d| + diff = d + end + + expect(diff.diff).to eq('') + end + end + + context 'when diff is quite large will collapse by default' do + let(:iterator) { [{ diff: 'a' * 20480 }] } + + context 'when no collapse is set' do + let(:no_collapse) { true } + + it 'yields Diff instances even when they are quite big' do + expect { |b| subject.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'does not prune diffs' do + diff = nil + + subject.each do |d| + diff = d + end + + expect(diff.diff).not_to eq('') + end + end + + context 'when no collapse is unset' do + let(:no_collapse) { false } + + it 'yields Diff instances even when they are quite big' do + expect { |b| subject.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'prunes diffs that are quite big' do + diff = nil + + subject.each do |d| + diff = d + end + + expect(diff.diff).to eq('') + end + + context 'when go over safe limits on files' do + let(:iterator) { [ fake_diff(1, 1) ] * 4 } + + before(:each) do + stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines }) + end + + it 'prunes diffs by default even little ones' do + subject.each_with_index do |d, i| + if i < 2 + expect(d.diff).not_to eq('') + else # 90 lines + expect(d.diff).to eq('') + end + end + end + end + + context 'when go over safe limits on lines' do + let(:iterator) do + [ + fake_diff(1, 45), + fake_diff(1, 45), + fake_diff(1, 20480), + fake_diff(1, 1) + ] + end + + before(:each) do + stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) + end + + it 'prunes diffs by default even little ones' do + subject.each_with_index do |d, i| + if i < 2 + expect(d.diff).not_to eq('') + else # 90 lines + expect(d.diff).to eq('') + end + end + end + end + + context 'when go over safe limits on bytes' do + let(:iterator) do + [ + fake_diff(1, 45), + fake_diff(1, 45), + fake_diff(1, 20480), + fake_diff(1, 1) + ] + end + + before(:each) do + stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) + end + + it 'prunes diffs by default even little ones' do + subject.each_with_index do |d, i| + if i < 2 + expect(d.diff).not_to eq('') + else # > 80 bytes + expect(d.diff).to eq('') + end + end + end + end + end + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + it 'yields Diff instances even when they are quite big' do + expect { |b| subject.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'does not prune diffs' do + diff = nil + + subject.each do |d| + diff = d + end + + expect(diff.diff).not_to eq('') + end + end + end + end + + def fake_diff(line_length, line_count) + { 'diff' => "#{'a' * line_length}\n" * line_count } + end +end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4c55532d165f7461c2a5648a6d02af2b08b69687 --- /dev/null +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -0,0 +1,287 @@ +require "spec_helper" + +describe Gitlab::Git::Diff, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + before do + @raw_diff_hash = { + diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""), + --- a/.gitmodules + +++ b/.gitmodules + @@ -4,3 +4,6 @@ + [submodule "gitlab-shell"] + \tpath = gitlab-shell + \turl = https://github.com/gitlabhq/gitlab-shell.git + +[submodule "gitlab-grack"] + + path = gitlab-grack + + url = https://gitlab.com/gitlab-org/gitlab-grack.git + +EOT + new_path: ".gitmodules", + old_path: ".gitmodules", + a_mode: '100644', + b_mode: '100644', + new_file: false, + renamed_file: false, + deleted_file: false, + too_large: false + } + + @rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: + [".gitmodules"]).patches.first + end + + describe '.new' do + context 'using a Hash' do + context 'with a small diff' do + let(:diff) { described_class.new(@raw_diff_hash) } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + it 'prunes the diff' do + diff = described_class.new(diff: 'a' * 204800) + + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + end + + context 'using a Rugged::Patch' do + context 'with a small diff' do + let(:diff) { described_class.new(@rugged_diff) } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil)) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + it 'prunes the diff' do + expect_any_instance_of(String).to receive(:bytesize). + and_return(1024 * 1024 * 1024) + + diff = described_class.new(@rugged_diff) + + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + + context 'using a collapsable diff that is too large' do + before do + # The patch total size is 200, with lines between 21 and 54. + # This is a quick-and-dirty way to test this. Ideally, a new patch is + # added to the test repo with a size that falls between the real limits. + stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150) + stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100) + end + + it 'prunes the diff as a large diff instead of as a collapsed diff' do + diff = described_class.new(@rugged_diff, collapse: true) + + expect(diff.diff).to be_empty + expect(diff).to be_too_large + expect(diff).not_to be_collapsed + end + end + + context 'using a large binary diff' do + it 'does not prune the diff' do + expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?). + and_return(true) + + diff = described_class.new(@rugged_diff) + + expect(diff.diff).not_to be_empty + end + end + end + end + + describe 'straight diffs' do + let(:options) { { straight: true } } + let(:diffs) { described_class.between(repository, 'feature', 'master', options) } + + it 'has the correct size' do + expect(diffs.size).to eq(24) + end + + context 'diff' do + it 'is an instance of Diff' do + expect(diffs.first).to be_kind_of(described_class) + end + + it 'has the correct new_path' do + expect(diffs.first.new_path).to eq('.DS_Store') + end + + it 'has the correct diff' do + expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ') + end + end + end + + describe '.between' do + let(:diffs) { described_class.between(repository, 'feature', 'master') } + subject { diffs } + + it { is_expected.to be_kind_of Gitlab::Git::DiffCollection } + + describe '#size' do + subject { super().size } + + it { is_expected.to eq(1) } + end + + context 'diff' do + subject { diffs.first } + + it { is_expected.to be_kind_of described_class } + + describe '#new_path' do + subject { super().new_path } + + it { is_expected.to eq('files/ruby/feature.rb') } + end + + describe '#diff' do + subject { super().diff } + + it { is_expected.to include '+class Feature' } + end + end + end + + describe '.filter_diff_options' do + let(:options) { { max_size: 100, invalid_opt: true } } + + context "without default options" do + let(:filtered_options) { described_class.filter_diff_options(options) } + + it "should filter invalid options" do + expect(filtered_options).not_to have_key(:invalid_opt) + end + end + + context "with default options" do + let(:filtered_options) do + default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true } + described_class.filter_diff_options(options, default_options) + end + + it "should filter invalid options" do + expect(filtered_options).not_to have_key(:invalid_opt) + expect(filtered_options).not_to have_key(:bad_opt) + end + + it "should merge with default options" do + expect(filtered_options).to have_key(:ignore_whitespace) + end + + it "should override default options" do + expect(filtered_options).to have_key(:max_size) + expect(filtered_options[:max_size]).to eq(100) + end + end + end + + describe '#submodule?' do + before do + commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + @diffs = commit.parents[0].diff(commit).patches + end + + it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) } + it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) } + end + + describe '#line_count' do + it 'returns the correct number of lines' do + diff = described_class.new(@rugged_diff) + + expect(diff.line_count).to eq(9) + end + end + + describe '#too_large?' do + it 'returns true for a diff that is too large' do + diff = described_class.new(diff: 'a' * 204800) + + expect(diff.too_large?).to eq(true) + end + + it 'returns false for a diff that is small enough' do + diff = described_class.new(diff: 'a') + + expect(diff.too_large?).to eq(false) + end + + it 'returns true for a diff that was explicitly marked as being too large' do + diff = described_class.new(diff: 'a') + + diff.prune_large_diff! + + expect(diff.too_large?).to eq(true) + end + end + + describe '#collapsed?' do + it 'returns false by default even on quite big diff' do + diff = described_class.new(diff: 'a' * 20480) + + expect(diff).not_to be_collapsed + end + + it 'returns false by default for a diff that is small enough' do + diff = described_class.new(diff: 'a') + + expect(diff).not_to be_collapsed + end + + it 'returns true for a diff that was explicitly marked as being collapsed' do + diff = described_class.new(diff: 'a') + + diff.prune_collapsed_diff! + + expect(diff).to be_collapsed + end + end + + describe '#collapsible?' do + it 'returns true for a diff that is quite large' do + diff = described_class.new(diff: 'a' * 20480) + + expect(diff).to be_collapsible + end + + it 'returns false for a diff that is small enough' do + diff = described_class.new(diff: 'a') + + expect(diff).not_to be_collapsible + end + end + + describe '#prune_collapsed_diff!' do + it 'prunes the diff' do + diff = described_class.new(diff: "foo\nbar") + + diff.prune_collapsed_diff! + + expect(diff.diff).to eq('') + expect(diff.line_count).to eq(0) + end + end +end diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8331153689384a4153151777b27c7201435de293 --- /dev/null +++ b/spec/lib/gitlab/git/encoding_helper_spec.rb @@ -0,0 +1,84 @@ +require "spec_helper" + +describe Gitlab::Git::EncodingHelper do + let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } } + let(:binary_string) { File.join(SEED_REPOSITORY_PATH, 'gitlab_logo.png') } + + describe '#encode!' do + [ + [ + 'leaves ascii only string as is', + 'ascii only string', + 'ascii only string' + ], + [ + 'leaves valid utf8 string as is', + 'multibyte string №∑∉', + 'multibyte string №∑∉' + ], + [ + 'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.', + "mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'), + "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ", + ], + ].each do |description, test_string, xpect| + it description do + expect(ext_class.encode!(test_string)).to eq(xpect) + end + end + + it 'leaves binary string as is' do + expect(ext_class.encode!(binary_string)).to eq(binary_string) + end + end + + describe '#encode_utf8' do + [ + [ + "encodes valid utf8 encoded string to utf8", + "λ, λ, λ".encode("UTF-8"), + "λ, λ, λ".encode("UTF-8"), + ], + [ + "encodes valid ASCII-8BIT encoded string to utf8", + "ascii only".encode("ASCII-8BIT"), + "ascii only".encode("UTF-8"), + ], + [ + "encodes valid ISO-8859-1 encoded string to utf8", + "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"), + "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"), + ], + ].each do |description, test_string, xpect| + it description do + r = ext_class.encode_utf8(test_string.force_encoding('UTF-8')) + expect(r).to eq(xpect) + expect(r.encoding.name).to eq('UTF-8') + end + end + end + + describe '#clean' do + [ + [ + 'leaves ascii only string as is', + 'ascii only string', + 'ascii only string' + ], + [ + 'leaves valid utf8 string as is', + 'multibyte string №∑∉', + 'multibyte string №∑∉' + ], + [ + 'removes invalid bytes from ASCII-8bit encoded multibyte string.', + "Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'), + "Lorem ipsum\n dolor sit amet, xyà yùabcdùefg", + ], + ].each do |description, test_string, xpect| + it description do + expect(ext_class.encode!(test_string)).to eq(xpect) + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2a915bf426f9849542c99f06b347f8e82e915fc5 --- /dev/null +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -0,0 +1,1184 @@ +require "spec_helper" + +describe Gitlab::Git::Repository, seed_helper: true do + include Gitlab::Git::EncodingHelper + + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + describe "Respond to" do + subject { repository } + + it { is_expected.to respond_to(:raw) } + it { is_expected.to respond_to(:rugged) } + it { is_expected.to respond_to(:root_ref) } + it { is_expected.to respond_to(:tags) } + end + + describe "#discover_default_branch" do + let(:master) { 'master' } + let(:feature) { 'feature' } + let(:feature2) { 'feature2' } + + it "returns 'master' when master exists" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master]) + expect(repository.discover_default_branch).to eq('master') + end + + it "returns non-master when master exists but default branch is set to something else" do + File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature') + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master]) + expect(repository.discover_default_branch).to eq('feature') + File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master') + end + + it "returns a non-master branch when only one exists" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature]) + expect(repository.discover_default_branch).to eq('feature') + end + + it "returns a non-master branch when more than one exists and master does not" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2]) + expect(repository.discover_default_branch).to eq('feature') + end + + it "returns nil when no branch exists" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([]) + expect(repository.discover_default_branch).to be_nil + end + end + + describe :branch_names do + subject { repository.branch_names } + + it 'has SeedRepo::Repo::BRANCHES.size elements' do + expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) + end + it { is_expected.to include("master") } + it { is_expected.not_to include("branch-from-space") } + end + + describe :tag_names do + subject { repository.tag_names } + + it { is_expected.to be_kind_of Array } + it 'has SeedRepo::Repo::TAGS.size elements' do + expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) + end + + describe '#last' do + subject { super().last } + it { is_expected.to eq("v1.2.1") } + end + it { is_expected.to include("v1.0.0") } + it { is_expected.not_to include("v5.0.0") } + end + + shared_examples 'archive check' do |extenstion| + it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) } + it { expect(metadata['ArchivePath']).to end_with extenstion } + end + + describe :archive do + let(:metadata) { repository.archive_metadata('master', '/tmp') } + + it_should_behave_like 'archive check', '.tar.gz' + end + + describe :archive_zip do + let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') } + + it_should_behave_like 'archive check', '.zip' + end + + describe :archive_bz2 do + let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') } + + it_should_behave_like 'archive check', '.tar.bz2' + end + + describe :archive_fallback do + let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') } + + it_should_behave_like 'archive check', '.tar.gz' + end + + describe :size do + subject { repository.size } + + it { is_expected.to be < 2 } + end + + describe :has_commits? do + it { expect(repository.has_commits?).to be_truthy } + end + + describe :empty? do + it { expect(repository.empty?).to be_falsey } + end + + describe :bare? do + it { expect(repository.bare?).to be_truthy } + end + + describe :heads do + let(:heads) { repository.heads } + subject { heads } + + it { is_expected.to be_kind_of Array } + + describe '#size' do + subject { super().size } + it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } + end + + context :head do + subject { heads.first } + + describe '#name' do + subject { super().name } + it { is_expected.to eq("feature") } + end + + context :commit do + subject { heads.first.dereferenced_target.sha } + + it { is_expected.to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } + end + end + end + + describe :ref_names do + let(:ref_names) { repository.ref_names } + subject { ref_names } + + it { is_expected.to be_kind_of Array } + + describe '#first' do + subject { super().first } + it { is_expected.to eq('feature') } + end + + describe '#last' do + subject { super().last } + it { is_expected.to eq('v1.2.1') } + end + end + + describe :search_files do + let(:results) { repository.search_files('rails', 'master') } + subject { results } + + it { is_expected.to be_kind_of Array } + + describe '#first' do + subject { super().first } + it { is_expected.to be_kind_of Gitlab::Git::BlobSnippet } + end + + context 'blob result' do + subject { results.first } + + describe '#ref' do + subject { super().ref } + it { is_expected.to eq('master') } + end + + describe '#filename' do + subject { super().filename } + it { is_expected.to eq('CHANGELOG') } + end + + describe '#startline' do + subject { super().startline } + it { is_expected.to eq(35) } + end + + describe '#data' do + subject { super().data } + it { is_expected.to include "Ability to filter by multiple labels" } + end + end + end + + context :submodules do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + context 'where repo has submodules' do + let(:submodules) { repository.submodules('master') } + let(:submodule) { submodules.first } + + it { expect(submodules).to be_kind_of Hash } + it { expect(submodules.empty?).to be_falsey } + + it 'should have valid data' do + expect(submodule).to eq([ + "six", { + "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", + "path" => "six", + "url" => "git://github.com/randx/six.git" + } + ]) + end + + it 'should handle nested submodules correctly' do + nested = submodules['nested/six'] + expect(nested['path']).to eq('nested/six') + expect(nested['url']).to eq('git://github.com/randx/six.git') + expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') + end + + it 'should handle deeply nested submodules correctly' do + nested = submodules['deeper/nested/six'] + expect(nested['path']).to eq('deeper/nested/six') + expect(nested['url']).to eq('git://github.com/randx/six.git') + expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') + end + + it 'should not have an entry for an invalid submodule' do + expect(submodules).not_to have_key('invalid/path') + end + + it 'should not have an entry for an uncommited submodule dir' do + submodules = repository.submodules('fix-existing-submodule-dir') + expect(submodules).not_to have_key('submodule-existing-dir') + end + + it 'should handle tags correctly' do + submodules = repository.submodules('v1.2.1') + + expect(submodules.first).to eq([ + "six", { + "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", + "path" => "six", + "url" => "git://github.com/randx/six.git" + } + ]) + end + end + + context 'where repo doesn\'t have submodules' do + let(:submodules) { repository.submodules('6d39438') } + it 'should return an empty hash' do + expect(submodules).to be_empty + end + end + end + + describe :commit_count do + it { expect(repository.commit_count("master")).to eq(25) } + it { expect(repository.commit_count("feature")).to eq(9) } + end + + describe "#reset" do + change_path = File.join(TEST_NORMAL_REPO_PATH, "CHANGELOG") + untracked_path = File.join(TEST_NORMAL_REPO_PATH, "UNTRACKED") + tracked_path = File.join(TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb") + + change_text = "New changelog text" + untracked_text = "This file is untracked" + + reset_commit = SeedRepo::LastCommit::ID + + context "--hard" do + before(:all) do + # Modify a tracked file + File.open(change_path, "w") do |f| + f.write(change_text) + end + + # Add an untracked file to the working directory + File.open(untracked_path, "w") do |f| + f.write(untracked_text) + end + + @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo.reset("HEAD", :hard) + end + + it "should replace the working directory with the content of the index" do + File.open(change_path, "r") do |f| + expect(f.each_line.first).not_to eq(change_text) + end + + File.open(tracked_path, "r") do |f| + expect(f.each_line.to_a[8]).to include('raise RuntimeError, "System commands') + end + end + + it "should not touch untracked files" do + expect(File.exist?(untracked_path)).to be_truthy + end + + it "should move the HEAD to the correct commit" do + new_head = @normal_repo.rugged.head.target.oid + expect(new_head).to eq(reset_commit) + end + + it "should move the tip of the master branch to the correct commit" do + new_tip = @normal_repo.rugged.references["refs/heads/master"]. + target.oid + + expect(new_tip).to eq(reset_commit) + end + + after(:all) do + # Fast-forward to the original HEAD + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + end + + describe "#checkout" do + new_branch = "foo_branch" + + context "-b" do + before(:all) do + @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo.checkout(new_branch, { b: true }, "origin/feature") + end + + it "should create a new branch" do + expect(@normal_repo.rugged.branches[new_branch]).not_to be_nil + end + + it "should move the HEAD to the correct commit" do + expect(@normal_repo.rugged.head.target.oid).to( + eq(@normal_repo.rugged.branches["origin/feature"].target.oid) + ) + end + + it "should refresh the repo's #heads collection" do + head_names = @normal_repo.heads.map { |h| h.name } + expect(head_names).to include(new_branch) + end + + after(:all) do + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + + context "without -b" do + context "and specifying a nonexistent branch" do + it "should not do anything" do + normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + + expect { normal_repo.checkout(new_branch) }.to raise_error(Rugged::ReferenceError) + expect(normal_repo.rugged.branches[new_branch]).to be_nil + expect(normal_repo.rugged.head.target.oid).to( + eq(normal_repo.rugged.branches["master"].target.oid) + ) + + head_names = normal_repo.heads.map { |h| h.name } + expect(head_names).not_to include(new_branch) + end + + after(:all) do + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + + context "and with a valid branch" do + before(:all) do + @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo.rugged.branches.create("feature", "origin/feature") + @normal_repo.checkout("feature") + end + + it "should move the HEAD to the correct commit" do + expect(@normal_repo.rugged.head.target.oid).to( + eq(@normal_repo.rugged.branches["feature"].target.oid) + ) + end + + it "should update the working directory" do + File.open(File.join(TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f| + expect(f.read.each_line.to_a).not_to include(".DS_Store\n") + end + end + + after(:all) do + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + end + end + + describe "#delete_branch" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.delete_branch("feature") + end + + it "should remove the branch from the repo" do + expect(@repo.rugged.branches["feature"]).to be_nil + end + + it "should update the repo's #heads collection" do + expect(@repo.heads).not_to include("feature") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#create_branch" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + end + + it "should create a new branch" do + expect(@repo.create_branch('new_branch', 'master')).not_to be_nil + end + + it "should create a new branch with the right name" do + expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch') + end + + it "should fail if we create an existing branch" do + @repo.create_branch('duplicated_branch', 'master') + expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + end + + it "should fail if we create a branch from a non existing ref" do + expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#remote_names" do + let(:remotes) { repository.remote_names } + + it "should have one entry: 'origin'" do + expect(remotes.size).to eq(1) + expect(remotes.first).to eq("origin") + end + end + + describe "#refs_hash" do + let(:refs) { repository.refs_hash } + + it "should have as many entries as branches and tags" do + expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS + # We flatten in case a commit is pointed at by more than one branch and/or tag + expect(refs.values.flatten.size).to eq(expected_refs.size) + end + end + + describe "#remote_delete" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.remote_delete("expendable") + end + + it "should remove the remote" do + expect(@repo.rugged.remotes).not_to include("expendable") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#remote_add" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.remote_add("new_remote", SeedHelper::GITLAB_URL) + end + + it "should add the remote" do + expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#remote_update" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) + end + + it "should add the remote" do + expect(@repo.rugged.remotes["expendable"].url).to( + eq(TEST_NORMAL_REPO_PATH) + ) + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#log" do + commit_with_old_name = nil + commit_with_new_name = nil + rename_commit = nil + + before(:all) do + # Add new commits so that there's a renamed file in the commit history + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + + commit_with_old_name = new_commit_edit_old_file(repo) + rename_commit = new_commit_move_file(repo) + commit_with_new_name = new_commit_edit_new_file(repo) + end + + context "where 'follow' == true" do + options = { ref: "master", follow: true } + + context "and 'path' is a directory" do + let(:log_commits) do + repository.log(options.merge(path: "encoding")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the new filename" do + let(:log_commits) do + repository.log(options.merge(path: "encoding/CHANGELOG")) + end + + it "should follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the old filename" do + let(:log_commits) do + repository.log(options.merge(path: "CHANGELOG")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_new_name) + end + end + + context "unknown ref" do + let(:log_commits) { repository.log(options.merge(ref: 'unknown')) } + + it "should return empty" do + expect(log_commits).to eq([]) + end + end + end + + context "where 'follow' == false" do + options = { follow: false } + + context "and 'path' is a directory" do + let(:log_commits) do + repository.log(options.merge(path: "encoding")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the new filename" do + let(:log_commits) do + repository.log(options.merge(path: "encoding/CHANGELOG")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the old filename" do + let(:log_commits) do + repository.log(options.merge(path: "CHANGELOG")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_new_name) + end + end + + context "and 'path' includes a directory that used to be a file" do + let(:log_commits) do + repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) + end + + it "should return a list of commits" do + expect(log_commits.size).to eq(1) + end + end + end + + context "compare results between log_by_walk and log_by_shell" do + let(:options) { { ref: "master" } } + let(:commits_by_walk) { repository.log(options).map(&:oid) } + let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:oid) } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + + context "with limit" do + let(:options) { { ref: "master", limit: 1 } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with offset" do + let(:options) { { ref: "master", offset: 1 } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with skip_merges" do + let(:options) { { ref: "master", skip_merges: true } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with path" do + let(:options) { { ref: "master", path: "encoding" } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + + context "with follow" do + let(:options) { { ref: "master", path: "encoding", follow: true } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + end + end + + context "where provides 'after' timestamp" do + options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } + + it "should returns commits on or after that timestamp" do + commits = repository.log(options) + + expect(commits.size).to be > 0 + satisfy do + commits.all? { |commit| commit.created_at >= options[:after] } + end + end + end + + context "where provides 'before' timestamp" do + options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } + + it "should returns commits on or before that timestamp" do + commits = repository.log(options) + + expect(commits.size).to be > 0 + satisfy do + commits.all? { |commit| commit.created_at <= options[:before] } + end + end + end + + after(:all) do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + end + end + + describe "#commits_between" do + context 'two SHAs' do + let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } + let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' } + + it 'returns the number of commits between' do + expect(repository.commits_between(first_sha, second_sha).count).to eq(3) + end + end + + context 'SHA and master branch' do + let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } + let(:branch) { 'master' } + + it 'returns the number of commits between a sha and a branch' do + expect(repository.commits_between(sha, branch).count).to eq(5) + end + + it 'returns the number of commits between a branch and a sha' do + expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch + end + end + + context 'two branches' do + let(:first_branch) { 'feature' } + let(:second_branch) { 'master' } + + it 'returns the number of commits between' do + expect(repository.commits_between(first_branch, second_branch).count).to eq(17) + end + end + end + + describe '#count_commits_between' do + subject { repository.count_commits_between('feature', 'master') } + + it { is_expected.to eq(17) } + end + + describe "branch_names_contains" do + subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) } + + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } + end + + describe '#autocrlf' do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.rugged.config['core.autocrlf'] = true + end + + it 'return the value of the autocrlf option' do + expect(@repo.autocrlf).to be(true) + end + + after(:all) do + @repo.rugged.config.delete('core.autocrlf') + end + end + + describe '#autocrlf=' do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.rugged.config['core.autocrlf'] = false + end + + it 'should set the autocrlf option to the provided option' do + @repo.autocrlf = :input + + File.open(File.join(TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file| + expect(config_file.read).to match('autocrlf = input') + end + end + + after(:all) do + @repo.rugged.config.delete('core.autocrlf') + end + end + + describe '#find_branch' do + it 'should return a Branch for master' do + branch = repository.find_branch('master') + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + + it 'should handle non-existent branch' do + branch = repository.find_branch('this-is-garbage') + + expect(branch).to eq(nil) + end + + it 'should reload Rugged::Repository and return master' do + expect(Rugged::Repository).to receive(:new).twice.and_call_original + + repository.find_branch('master') + branch = repository.find_branch('master', force_reload: true) + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + end + + describe '#branches with deleted branch' do + before(:each) do + ref = double() + allow(ref).to receive(:name) { 'bad-branch' } + allow(ref).to receive(:target) { raise Rugged::ReferenceError } + allow(repository.rugged).to receive(:branches) { [ref] } + end + + it 'should return empty branches' do + expect(repository.branches).to eq([]) + end + end + + describe '#branch_count' do + before(:each) do + valid_ref = double(:ref) + invalid_ref = double(:ref) + + allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) + + allow(invalid_ref).to receive_messages(name: 'bad-branch') + allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } + + allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) + end + + it 'returns the number of branches' do + expect(repository.branch_count).to eq(1) + end + end + + describe '#mkdir' do + let(:commit_options) do + { + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Test message', + branch: 'refs/heads/fix', + } + } + end + + def generate_diff_for_path(path) + "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep +new file mode 100644 +index 0000000..e69de29 +--- /dev/null ++++ b/#{path}/.gitkeep\n" + end + + shared_examples 'mkdir diff check' do |path, expected_path| + it 'creates a directory' do + result = repository.mkdir(path, commit_options) + expect(result).not_to eq(nil) + + # Verify another mkdir doesn't create a directory that already exists + expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists') + end + end + + describe 'creates a directory in root directory' do + it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir' + end + + describe 'creates a directory in subdirectory' do + it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test' + end + + describe 'creates a directory in subdirectory with a slash' do + it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2' + end + + describe 'creates a directory in subdirectory with multiple slashes' do + it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3' + end + + describe 'handles relative paths' do + it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative' + end + + describe 'creates nested directories' do + it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test' + end + + it 'does not attempt to create a directory with invalid relative path' do + expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path') + end + + it 'does not attempt to overwrite a file' do + expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file') + end + + it 'does not attempt to overwrite a directory' do + expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists') + end + end + + describe "#ls_files" do + let(:master_file_paths) { repository.ls_files("master") } + let(:not_existed_branch) { repository.ls_files("not_existed_branch") } + + it "read every file paths of master branch" do + expect(master_file_paths.length).to equal(40) + end + + it "reads full file paths of master branch" do + expect(master_file_paths).to include("files/html/500.html") + end + + it "dose not read submodule directory and empty directory of master branch" do + expect(master_file_paths).not_to include("six") + end + + it "does not include 'nil'" do + expect(master_file_paths).not_to include(nil) + end + + it "returns empty array when not existed branch" do + expect(not_existed_branch.length).to equal(0) + end + end + + describe "#copy_gitattributes" do + let(:attributes_path) { File.join(TEST_REPO_PATH, 'info/attributes') } + + it "raises an error with invalid ref" do + expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) + end + + context "with no .gitattrbutes" do + before(:each) do + repository.copy_gitattributes("master") + end + + it "does not have an info/attributes" do + expect(File.exist?(attributes_path)).to be_falsey + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + + context "with .gitattrbutes" do + before(:each) do + repository.copy_gitattributes("gitattributes") + end + + it "has an info/attributes" do + expect(File.exist?(attributes_path)).to be_truthy + end + + it "has the same content in info/attributes as .gitattributes" do + contents = File.open(attributes_path, "rb") { |f| f.read } + expect(contents).to eq("*.md binary\n") + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + + context "with updated .gitattrbutes" do + before(:each) do + repository.copy_gitattributes("gitattributes") + repository.copy_gitattributes("gitattributes-updated") + end + + it "has an info/attributes" do + expect(File.exist?(attributes_path)).to be_truthy + end + + it "has the updated content in info/attributes" do + contents = File.read(attributes_path) + expect(contents).to eq("*.txt binary\n") + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + + context "with no .gitattrbutes in HEAD but with previous info/attributes" do + before(:each) do + repository.copy_gitattributes("gitattributes") + repository.copy_gitattributes("master") + end + + it "does not have an info/attributes" do + expect(File.exist?(attributes_path)).to be_falsey + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + end + + describe '#diffable' do + info_dir_path = attributes_path = File.join(TEST_REPO_PATH, 'info') + attributes_path = File.join(info_dir_path, 'attributes') + + before(:all) do + FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) + File.write(attributes_path, "*.md -diff\n") + end + + it "should return true for files which are text and do not have attributes" do + blob = Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'LICENSE' + ) + expect(repository.diffable?(blob)).to be_truthy + end + + it "should return false for binary files which do not have attributes" do + blob = Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/images/logo-white.png' + ) + expect(repository.diffable?(blob)).to be_falsey + end + + it "should return false for text files which have been marked as not being diffable in attributes" do + blob = Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'README.md' + ) + expect(repository.diffable?(blob)).to be_falsey + end + + after(:all) do + FileUtils.rm_rf(info_dir_path) + end + end + + describe '#tag_exists?' do + it 'returns true for an existing tag' do + tag = repository.tag_names.first + + expect(repository.tag_exists?(tag)).to eq(true) + end + + it 'returns false for a non-existing tag' do + expect(repository.tag_exists?('v9000')).to eq(false) + end + end + + describe '#branch_exists?' do + it 'returns true for an existing branch' do + expect(repository.branch_exists?('master')).to eq(true) + end + + it 'returns false for a non-existing branch' do + expect(repository.branch_exists?('kittens')).to eq(false) + end + + it 'returns false when using an invalid branch name' do + expect(repository.branch_exists?('.bla')).to eq(false) + end + end + + describe '#local_branches' do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + + it 'returns the local branches' do + create_remote_branch('joe', 'remote_branch', 'master') + @repo.create_branch('local_branch', 'master') + + expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) + expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) + end + end + + def create_remote_branch(remote_name, branch_name, source_branch_name) + source_branch = @repo.branches.find { |branch| branch.name == source_branch_name } + rugged = @repo.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) + end + + # Build the options hash that's passed to Rugged::Commit#create + def commit_options(repo, index, message) + options = {} + options[:tree] = index.write_tree(repo) + options[:author] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:committer] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:message] ||= message + options[:parents] = repo.empty? ? [] : [repo.head.target].compact + options[:update_ref] = "HEAD" + + options + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of CHANGELOG with a single new line of text. + def new_commit_edit_old_file(repo) + oid = repo.write("I replaced the changelog with this text", :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "CHANGELOG", oid: oid, mode: 0100644) + + options = commit_options( + repo, + index, + "Edit CHANGELOG in its original location" + ) + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of encoding/CHANGELOG with new text. + def new_commit_edit_new_file(repo) + oid = repo.write("I'm a new changelog with different text", :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) + + options = commit_options(repo, index, "Edit encoding/CHANGELOG") + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Moves the + # CHANGELOG file to the encoding/ directory. + def new_commit_move_file(repo) + blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] + file_content = repo.lookup(blob_oid).content + oid = repo.write(file_content, :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) + index.remove("CHANGELOG") + + options = commit_options(repo, index, "Move CHANGELOG to encoding/") + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end +end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1f9c987be0bc8b17d223d075ccdae0072427151b --- /dev/null +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Gitlab::Git::RevList, lib: true do + let(:project) { create(:project) } + + context "validations" do + described_class::ALLOWED_VARIABLES.each do |var| + context var do + it "accepts values starting with the project repo path" do + env = { var => "#{project.repository.path_to_repo}/objects" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).to be_valid + end + + it "rejects values starting not with the project repo path" do + env = { var => "/some/other/path" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).not_to be_valid + end + + it "rejects values containing the project repo path but not starting with it" do + env = { var => "/some/other/path/#{project.repository.path_to_repo}" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).not_to be_valid + end + + it "ignores nil values" do + env = { var => nil } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).to be_valid + end + end + end + end + + context "#execute" do + let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } } + let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) } + + it "calls out to `popen` without environment variables if the record is invalid" do + allow(rev_list).to receive(:valid?).and_return(false) + + expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args) + + rev_list.execute + end + + it "calls out to `popen` with environment variables if the record is valid" do + allow(rev_list).to receive(:valid?).and_return(true) + + expect(Open3).to receive(:popen3).with(hash_including(env), any_args) + + rev_list.execute + end + end +end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ad469e947358e97c6fa20c88616e12e6c0937306 --- /dev/null +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -0,0 +1,25 @@ +require "spec_helper" + +describe Gitlab::Git::Tag, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + describe 'first tag' do + let(:tag) { repository.tags.first } + + it { expect(tag.name).to eq("v1.0.0") } + it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") } + it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") } + it { expect(tag.message).to eq("Release") } + end + + describe 'last tag' do + let(:tag) { repository.tags.last } + + it { expect(tag.name).to eq("v1.2.1") } + it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") } + it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") } + it { expect(tag.message).to eq("Version 1.2.1") } + end + + it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) } +end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..688e2a753732b37a2845bf3c5ca0c526d18dd137 --- /dev/null +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -0,0 +1,76 @@ +require "spec_helper" + +describe Gitlab::Git::Tree, seed_helper: true do + context :repo do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } + + it { expect(tree).to be_kind_of Array } + it { expect(tree.empty?).to be_falsey } + it { expect(tree.select(&:dir?).size).to eq(2) } + it { expect(tree.select(&:file?).size).to eq(10) } + it { expect(tree.select(&:submodule?).size).to eq(2) } + + describe :dir do + let(:dir) { tree.select(&:dir?).first } + + it { expect(dir).to be_kind_of Gitlab::Git::Tree } + it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') } + it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(dir.name).to eq('encoding') } + it { expect(dir.path).to eq('encoding') } + + context :subdir do + let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first } + + it { expect(subdir).to be_kind_of Gitlab::Git::Tree } + it { expect(subdir.id).to eq('a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba') } + it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(subdir.name).to eq('html') } + it { expect(subdir.path).to eq('files/html') } + end + + context :subdir_file do + let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } + + it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree } + it { expect(subdir_file.id).to eq('7e3e39ebb9b2bf433b4ad17313770fbe4051649c') } + it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(subdir_file.name).to eq('popen.rb') } + it { expect(subdir_file.path).to eq('files/ruby/popen.rb') } + end + end + + describe :file do + let(:file) { tree.select(&:file?).first } + + it { expect(file).to be_kind_of Gitlab::Git::Tree } + it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') } + it { expect(file.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(file.name).to eq('.gitignore') } + end + + describe :readme do + let(:file) { tree.select(&:readme?).first } + + it { expect(file).to be_kind_of Gitlab::Git::Tree } + it { expect(file.name).to eq('README.md') } + end + + describe :contributing do + let(:file) { tree.select(&:contributing?).first } + + it { expect(file).to be_kind_of Gitlab::Git::Tree } + it { expect(file.name).to eq('CONTRIBUTING.md') } + end + + describe :submodule do + let(:submodule) { tree.select(&:submodule?).first } + + it { expect(submodule).to be_kind_of Gitlab::Git::Tree } + it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') } + it { expect(submodule.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + it { expect(submodule.name).to eq('gitlab-shell') } + end + end +end diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d43b570e98841c65ffc9cd1be694dcd72312a31 --- /dev/null +++ b/spec/lib/gitlab/git/util_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::Git::Util do + describe :count_lines do + [ + ["", 0], + ["foo", 1], + ["foo\n", 1], + ["foo\n\n", 2], + ].each do |string, line_count| + it "counts #{line_count} lines in #{string.inspect}" do + expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count) + end + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index f1d0a190002b246a5aae7df3da0154f4bf6f4c39..44b84afde471e00c3f5a671898dd61b1318a2d8e 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::GitAccess, lib: true do end end - describe 'download_access_check' do + describe '#check_download_access!' do subject { access.check('git-upload-pack', '_any') } describe 'master permissions' do @@ -82,7 +82,7 @@ describe Gitlab::GitAccess, lib: true do end end - describe 'without acccess to project' do + describe 'without access to project' do context 'pull code' do it { expect(subject.allowed?).to be_falsey } end @@ -112,7 +112,7 @@ describe Gitlab::GitAccess, lib: true do end describe 'deploy key permissions' do - let(:key) { create(:deploy_key) } + let(:key) { create(:deploy_key, user: user) } let(:actor) { key } context 'pull code' do @@ -136,7 +136,7 @@ describe Gitlab::GitAccess, lib: true do end context 'from private project' do - let(:project) { create(:project, :internal) } + let(:project) { create(:project, :private) } it { expect(subject).not_to be_allowed } end @@ -183,7 +183,7 @@ describe Gitlab::GitAccess, lib: true do end end - describe 'push_access_check' do + describe '#check_push_access!' do before { merge_into_protected_branch } let(:unprotected_branch) { FFaker::Internet.user_name } @@ -231,7 +231,7 @@ describe Gitlab::GitAccess, lib: true do permissions_matrix[role].each do |action, allowed| context action do - subject { access.push_access_check(changes[action]) } + subject { access.send(:check_push_access!, changes[action]) } it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } end end @@ -353,13 +353,13 @@ describe Gitlab::GitAccess, lib: true do end end - shared_examples 'can not push code' do + shared_examples 'pushing code' do |can| subject { access.check('git-receive-pack', '_any') } context 'when project is authorized' do before { authorize } - it { expect(subject).not_to be_allowed } + it { expect(subject).public_send(can, be_allowed) } end context 'when unauthorized' do @@ -386,7 +386,7 @@ describe Gitlab::GitAccess, lib: true do describe 'build authentication abilities' do let(:authentication_abilities) { build_authentication_abilities } - it_behaves_like 'can not push code' do + it_behaves_like 'pushing code', :not_to do def authorize project.team << [user, :reporter] end @@ -394,12 +394,26 @@ describe Gitlab::GitAccess, lib: true do end describe 'deploy key permissions' do - let(:key) { create(:deploy_key) } + let(:key) { create(:deploy_key, user: user, can_push: can_push) } let(:actor) { key } - it_behaves_like 'can not push code' do - def authorize - key.projects << project + context 'when deploy_key can push' do + let(:can_push) { true } + + it_behaves_like 'pushing code', :to do + def authorize + key.projects << project + end + end + end + + context 'when deploy_key cannot push' do + let(:can_push) { false } + + it_behaves_like 'pushing code', :not_to do + def authorize + key.projects << project + end end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 578db51631e194d183da6596974c952bca162543..a5d172233cc33aa99241fe82ef1663b2d5c7bd25 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::GitAccessWiki, lib: true do ['6f6d7e7ed 570e7b2ab refs/heads/master'] end - describe '#download_access_check' do + describe '#access_check_download!' do subject { access.check('git-upload-pack', '_any') } before do diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index e829b93634333a21234f841c4a300ba3e18f288c..21f2a9e225b99280f3a754abcb80b92daffd0d18 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do end end - context 'when provider does not specity an API endpoint' do - it 'uses GitHub root API endpoint' do - expect(client.api.api_endpoint).to eq 'https://api.github.com/' + describe '#api_endpoint' do + context 'when provider does not specity an API endpoint' do + it 'uses GitHub root API endpoint' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end end - end - context 'when provider specify a custom API endpoint' do - before do - github_provider['args']['client_options']['site'] = 'https://github.company.com/' + context 'when provider specify a custom API endpoint' do + before do + github_provider['args']['client_options']['site'] = 'https://github.company.com/' + end + + it 'uses the custom API endpoint' do + expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) + expect(client.api.api_endpoint).to eq 'https://github.company.com/' + end + end + + context 'when given a host' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') } + + it 'builds a endpoint with the given host and the default API version' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end - it 'uses the custom API endpoint' do - expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) - expect(client.api.api_endpoint).to eq 'https://github.company.com/' + context 'when given an API version' do + subject(:client) { described_class.new(token, api_version: 'v3') } + + it 'does not use the API version without a host' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end + end + + context 'when given a host and version' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') } + + it 'builds a endpoint with the given options' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 9e027839f592931b286c1af5b687b4eed548e40a..72421832ffc38d43bdc1800b3b1cbd3928f7cc1e 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -1,169 +1,251 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do - describe '#execute' do + shared_examples 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [] } + before do - allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + allow(project).to receive(:import_data).and_return(double.as_null_object) end - context 'when an error occurs' do - let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } - let(:octocat) { double(id: 123456, login: 'octocat') } - let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } - let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } - let(:repository) { double(id: 1, fork: false) } - let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - - let(:label1) do - double( - name: 'Bug', - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + it 'calls import methods' do + importer = described_class.new(project) - let(:label2) do - double( - name: nil, - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + expected_called = [ + :import_labels, :import_milestones, :import_pull_requests, :import_issues, + :import_wiki, :import_releases, :handle_errors + ] - let(:milestone) do - double( - number: 1347, - state: 'open', - title: '1.0', - description: 'Version 1.0', - due_on: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1' - ) - end + expected_called -= expected_not_called - let(:issue1) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'Found a bug', - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347', - labels: [double(name: 'Label #1')], - ) - end + aggregate_failures do + expected_called.each do |method_name| + expect(importer).to receive(method_name) + end - let(:issue2) do - double( - number: 1348, - milestone: nil, - state: 'open', - title: nil, - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348', - labels: [double(name: 'Label #2')], - ) - end + expect(importer).to receive(:import_comments).with(:issues) + expect(importer).to receive(:import_comments).with(:pull_requests) - let(:pull_request) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'New feature', - body: 'Please pull these awesome changes', - head: source_branch, - base: target_branch, - assignee: nil, - user: octocat, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - merged_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347', - ) + expected_not_called.each do |method_name| + expect(importer).not_to receive(method_name) + end end - let(:release1) do - double( - tag_name: 'v1.0.0', - name: 'First release', - body: 'Release v1.0.0', - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/1' - ) - end + importer.execute + end + end - let(:release2) do - double( - tag_name: 'v2.0.0', - name: 'Second release', - body: nil, - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/2' - ) - end + shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do + before do + allow(project).to receive(:import_data).and_return(double.as_null_object) - before do - allow(project).to receive(:import_data).and_return(double.as_null_object) - allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) - allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) - allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) - allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) - allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) - allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) - allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) - end + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + + allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + + allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) + allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) + allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) + allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) + allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) + end + let(:octocat) { double(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:label1) do + double( + name: 'Bug', + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:label2) do + double( + name: nil, + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:milestone) do + double( + id: 1347, # For Gitea + number: 1347, + state: 'open', + title: '1.0', + description: 'Version 1.0', + due_on: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/milestones/1" + ) + end - it 'returns true' do - expect(described_class.new(project).execute).to eq true + let(:issue1) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1347", + labels: [double(name: 'Label #1')] + ) + end + + let(:issue2) do + double( + number: 1348, + milestone: nil, + state: 'open', + title: nil, + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1348", + labels: [double(name: 'Label #2')] + ) + end + + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + + let(:release1) do + double( + tag_name: 'v1.0.0', + name: 'First release', + body: 'Release v1.0.0', + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/1" + ) + end + + let(:release2) do + double( + tag_name: 'v2.0.0', + name: 'Second release', + body: nil, + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/2" + ) + end + + it 'returns true' do + expect(described_class.new(project).execute).to eq true + end + + it 'does not raise an error' do + expect { described_class.new(project).execute }.not_to raise_error + end + + it 'stores error messages' do + error = { + message: 'The remote data could not be fully imported.', + errors: [ + { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, + { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, + { type: :wiki, errors: "Gitlab::Shell::Error" } + ] + } + + unless project.gitea_import? + error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" } end - it 'does not raise an error' do - expect { described_class.new(project).execute }.not_to raise_error + described_class.new(project).execute + + expect(project.import_error).to eq error.to_json + end + end + + let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) } + let(:credentials) { { user: 'joe' } } + + context 'when importing a GitHub project' do + let(:api_root) { 'https://api.github.com' } + let(:repo_root) { 'https://github.com' } + + it_behaves_like 'Gitlab::GithubImport::Importer#execute' + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + {} + ) + + described_class.new(project).client end + end + end - it 'stores error messages' do - error = { - message: 'The remote data could not be fully imported.', - errors: [ - { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, - { type: :wiki, errors: "Gitlab::Shell::Error" }, - { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } - ] - } + context 'when importing a Gitea project' do + let(:api_root) { 'https://try.gitea.io/api/v1' } + let(:repo_root) { 'https://try.gitea.io' } + before do + project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") + end - described_class.new(project).execute + it_behaves_like 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [:import_releases] } + end + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + { host: "#{repo_root}:443/foo", api_version: 'v1' } + ) - expect(project.import_error).to eq error.to_json + described_class.new(project).client end end end diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6bc5f98ed2c6c039af634ef49b45e77de84d0a50 --- /dev/null +++ b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssuableFormatter, lib: true do + let(:raw_data) do + double(number: 42) + end + let(:project) { double(import_type: 'github') } + let(:issuable_formatter) { described_class.new(project, raw_data) } + + describe '#project_association' do + it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) } + end + + describe '#number' do + it { expect(issuable_formatter.number).to eq(42) } + end + + describe '#find_condition' do + it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) } + end +end diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index 95339e2f128933791ba47e94bafd9d653c4e1c94..e31ed9c1fa06d9c6415499739ee9550d9d73dc7b 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data)} + subject(:issue) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(issue.attributes.fetch(:milestone)).to eq milestone end @@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end + shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do + let(:raw_data) { double(base_data.merge(number: 1347)) } + + it 'returns issue number' do + expect(issue.number).to eq 1347 + end + end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + describe '#has_comments?' do context 'when number of comments is greater than zero' do let(:raw_data) { double(base_data.merge(comments: 1)) } @@ -136,14 +158,6 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } - - it 'returns pull request number' do - expect(issue.number).to eq 1347 - end - end - describe '#pull_request?' do context 'when mention a pull request' do let(:raw_data) { double(base_data.merge(pull_request: double)) } diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 09337c99a07952c65ad85d34e2df2d6ce7cd9d81..6d38041c46875096fa2c7f4bd9982d9d3711c871 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { - number: 1347, state: 'open', title: '1.0', description: 'Version 1.0', @@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do closed_at: nil } end + let(:iid_attr) { :number } - subject(:formatter) { described_class.new(project, raw_data)} + subject(:formatter) { described_class.new(project, raw_data) } + + shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do + let(:data) { base_data.merge(iid_attr => 1347) } - describe '#attributes' do context 'when milestone is open' do - let(:raw_data) { double(base_data.merge(state: 'open')) } + let(:raw_data) { double(data.merge(state: 'open')) } it 'returns formatted attributes' do expected = { @@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:raw_data) { double(base_data.merge(state: 'closed')) } + let(:raw_data) { double(data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do context 'when milestone has a due date' do let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(due_on: due_date)) } + let(:raw_data) { double(data.merge(due_on: due_date)) } it 'returns formatted attributes' do expected = { @@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end end end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end + + context 'when importing a Gitea project' do + let(:iid_attr) { :id } + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 302f0fc06236fbffeeb5a95001ba71d518d50f61..2b3256edcb239dac62ad450eac9d7225afd685de 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data)} + subject(:pull_request) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(pull_request.attributes.fetch(:milestone)).to eq milestone end end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do + let(:raw_data) { double(base_data) } it 'returns pull request number' do expect(pull_request.number).to eq 1347 end end - describe '#source_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end - describe '#target_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8e1a28f272346c1c2ed3275c4eba32ad4a8c8ab8..7fb6829f582f2514459b6644c05a0af35ade83a3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -15,6 +15,7 @@ issues: - events - merge_requests_closing_issues - metrics +- timelogs events: - author - project @@ -77,6 +78,7 @@ merge_requests: - events - merge_requests_closing_issues - metrics +- timelogs merge_request_diff: - merge_request pipelines: @@ -129,6 +131,7 @@ project: - builds_email_service - pipelines_email_service - mattermost_slash_commands_service +- slack_slash_commands_service - irker_service - pivotaltracker_service - hipchat_service @@ -137,6 +140,7 @@ project: - asana_service - gemnasium_service - slack_service +- mattermost_service - buildkite_service - bamboo_service - teamcity_service @@ -147,6 +151,7 @@ project: - bugzilla_service - gitlab_issue_tracker_service - external_wiki_service +- kubernetes_service - forked_project_link - forked_from_project - forked_project_links @@ -189,8 +194,12 @@ project: - authorized_users - project_authorizations - route +- statistics award_emoji: - awardable - user priorities: - label +timelogs: +- trackable +- user diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index 5ae178414cc240f529a55e96e557b4c64956f61d..08a42fd27a255bf1577adc61f75b5695734ad3ff 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer, lib: true do + include UploadHelpers + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:project) { create(:empty_project) } before do allow_any_instance_of(described_class).to receive(:avatar_export_file) - .and_return(Rails.root + "spec/fixtures/dk.png") + .and_return(uploaded_image_temp_path) end after do diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index ed9df468cede2a66f28fa23296655a79b553eb0c..2c0750c3377ee3ef78ce99d4b10243be1999d603 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -15,6 +15,28 @@ "type": "ProjectLabel", "priorities": [ ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] } ], "issues": [ @@ -2517,7 +2539,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_build_succeeds": true, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -6548,7 +6570,9 @@ "url": null }, "erased_by_id": null, - "erased_at": null + "erased_at": null, + "type": "Ci::Build", + "token": "abcd" }, { "id": 72, @@ -7409,6 +7433,28 @@ "category": "common", "default": false, "wiki_page_events": true + }, + { + "id": 101, + "title": "JenkinsDeprecated", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.031Z", + "updated_at": "2016-06-14T15:01:51.031Z", + "active": false, + "properties": { + + }, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "build_events": true, + "category": "common", + "default": false, + "wiki_page_events": true, + "type": "JenkinsDeprecatedService" } ], "hooks": [ 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 3038ab53ad8bf945769ab7559ecafe213dd0de54..4b07fa53bf509b934bc015cb602f8bb0f4b4f497 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -189,6 +189,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end end end + + context 'when there is an existing build with build token' do + it 'restores project json correctly' do + create(:ci_build, token: 'abcd') + + expect(restored_project_json).to be true + end + end end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 78d6b2c5032de2dbda6ba32e7945583bd65e5a1f..493bc2db21af108def3c04b6bb3fbac414a990e8 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -20,6 +20,7 @@ Issue: - lock_version - milestone_id - weight +- time_estimate Event: - id - target_type @@ -150,6 +151,7 @@ MergeRequest: - milestone_id - approvals_before_merge - rebase_commit_sha +- time_estimate MergeRequestDiff: - id - state @@ -247,6 +249,8 @@ DeployKey: - type - fingerprint - public +- can_push +- last_used_at Service: - id - type @@ -342,3 +346,11 @@ LabelPriority: - priority - created_at - updated_at +Timelog: +- id +- time_spent +- trackable_id +- trackable_type +- user_id +- created_at +- updated_at diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8cea38e9ff85fd40f610a1b668c323ae1116cdfd --- /dev/null +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::ImportSources do + describe '.options' do + it 'returns a hash' do + expected = + { + 'GitHub' => 'github', + 'Bitbucket' => 'bitbucket', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea' + } + + expect(described_class.options).to eq(expected) + end + end + + describe '.values' do + it 'returns an array' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'git', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.values).to eq(expected) + end + end + + describe '.importer_names' do + it 'returns an array of importer names' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.importer_names).to eq(expected) + end + end + + describe '.importer' do + import_sources = { + 'github' => Gitlab::GithubImport::Importer, + 'bitbucket' => Gitlab::BitbucketImport::Importer, + 'gitlab' => Gitlab::GitlabImport::Importer, + 'google_code' => Gitlab::GoogleCodeImport::Importer, + 'fogbugz' => Gitlab::FogbugzImport::Importer, + 'git' => nil, + 'gitlab_project' => Gitlab::ImportExport::Importer, + 'gitea' => Gitlab::GithubImport::Importer + } + + import_sources.each do |name, klass| + it "returns #{klass} when given #{name}" do + expect(described_class.importer(name)).to eq(klass) + end + end + end + + describe '.title' do + import_sources = { + 'github' => 'GitHub', + 'bitbucket' => 'Bitbucket', + 'gitlab' => 'GitLab.com', + 'google_code' => 'Google Code', + 'fogbugz' => 'FogBugz', + 'git' => 'Repo by URL', + 'gitlab_project' => 'GitLab export', + 'gitea' => 'Gitea' + } + + import_sources.each do |name, title| + it "returns #{title} when given #{name}" do + expect(described_class.title(name)).to eq(title) + end + end + end +end diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c9bd52a3b8fa85acac312ba5673a6fb9b701b526 --- /dev/null +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes do + include described_class + + describe '#container_exec_url' do + let(:api_url) { 'https://example.com' } + let(:namespace) { 'default' } + let(:pod_name) { 'pod1' } + let(:container_name) { 'container1' } + + subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) } + + it { expect(result.scheme).to eq('wss') } + it { expect(result.host).to eq('example.com') } + it { expect(result.path).to eq('/api/v1/namespaces/default/pods/pod1/exec') } + it { expect(result.query).to eq('container=container1&stderr=true&stdin=true&stdout=true&tty=true&command=sh&command=-c&command=bash+%7C%7C+sh') } + + context 'with a HTTP API URL' do + let(:api_url) { 'http://example.com' } + + it { expect(result.scheme).to eq('ws') } + end + + context 'with a path prefix in the API URL' do + let(:api_url) { 'https://example.com/prefix/' } + it { expect(result.path).to eq('/prefix/api/v1/namespaces/default/pods/pod1/exec') } + end + + context 'with arguments that need urlencoding' do + let(:namespace) { 'default namespace' } + let(:pod_name) { 'pod 1' } + let(:container_name) { 'container 1' } + + it { expect(result.path).to eq('/api/v1/namespaces/default%20namespace/pods/pod%201/exec') } + it { expect(result.query).to match(/\Acontainer=container\+1&/) } + end + end +end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 534bcbf39febdbaf50426fdb90240a5058ee9b6b..b9d12c3c24c18dfc5ce1239d359842e810c122be 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'should block user in GitLab' do + expect(access).to receive(:block_user).with(user, 'does not exist anymore') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'blocks user in GitLab' do + expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do end it 'does not unblock user in GitLab' do + expect(access).not_to receive(:unblock_user) + access.allowed? + expect(user).to be_blocked expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic end @@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do end it 'unblocks user in GitLab' do + expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore') + access.allowed? - expect(user).not_to be_blocked end end end @@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'blocks user in GitLab' do + expect(access).to receive(:block_user).with(user, 'does not exist anymore') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do end it 'unblocks the user if it exists' do + expect(access).to receive(:unblock_user).with(user, 'is available again') + access.allowed? - expect(user).not_to be_blocked end end end end end + + describe '#block_user' do + before do + user.activate + allow(Gitlab::AppLogger).to receive(:info) + + access.block_user user, 'reason' + end + + it 'blocks the user' do + expect(user).to be_blocked + expect(user).to be_ldap_blocked + end + + it 'logs the reason' do + expect(Gitlab::AppLogger).to have_received(:info).with( + "LDAP account \"123456\" reason, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + end + + describe '#unblock_user' do + before do + user.ldap_block + allow(Gitlab::AppLogger).to receive(:info) + + access.unblock_user user, 'reason' + end + + it 'activates the user' do + expect(user).not_to be_blocked + expect(user).not_to be_ldap_blocked + end + + it 'logs the reason' do + Gitlab::AppLogger.info( + "LDAP account \"123456\" reason, " \ + "unblocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + end end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 1a6803e01c32799ec5e6fe1d8bec8f456c5fdb47..cab2e9908fff0f68da83d7319177e3588d5ea3bd 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -129,4 +129,27 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.has_auth?).to be_falsey end end + + describe '#attributes' do + it 'uses default attributes when no custom attributes are configured' do + expect(config.attributes).to eq(config.default_attributes) + end + + it 'merges the configuration attributes with default attributes' do + stub_ldap_config( + options: { + 'attributes' => { + 'username' => %w(sAMAccountName), + 'email' => %w(userPrincipalName) + } + } + ) + + expect(config.attributes).to include({ + 'username' => %w(sAMAccountName), + 'email' => %w(userPrincipalName), + 'name' => 'cn' + }) + end + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9a556cde5d56bc9872eb7f0637c38ce6735a97cc --- /dev/null +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::LDAP::Person do + include LdapHelpers + + let(:entry) { ldap_user_entry('john.doe') } + + before do + stub_ldap_config( + options: { + 'attributes' => { + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName) + } + } + ) + end + + describe '#name' do + it 'uses the configured name attribute and handles values as an array' do + name = 'John Doe' + entry['cn'] = [name] + person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + + expect(person.name).to eq(name) + end + end + + describe '#email' do + it 'returns the value of mail, if present' do + mail = 'john@example.com' + entry['mail'] = mail + person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + + expect(person.email).to eq([mail]) + end + + it 'returns the value of userPrincipalName, if mail and email are not present' do + user_principal_name = 'john.doe@example.com' + entry['userPrincipalName'] = user_principal_name + person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + + expect(person.email).to eq([user_principal_name]) + end + end +end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index bcaffd279090309ca3a141327e3f5bade87af6ce..fb470ea75684f567dd05e86106e7742f44272207 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do end it 'tags a transaction with the method and path of the route in the grape endpoint' do - route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") + route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) env['api.endpoint'] = endpoint @@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do let(:transaction) { middleware.transaction_from_env(env) } it 'tags a transaction with the method and path of the route in the grape endpount' do - route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") + route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) env['api.endpoint'] = endpoint @@ -126,5 +126,16 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction.action).to eq('Grape#GET /projects/:id/archive') end + + it 'does not tag a transaction if route infos are missing' do + endpoint = double(:endpoint) + allow(endpoint).to receive(:route).and_raise + + env['api.endpoint'] = endpoint + + middleware.tag_endpoint(transaction, env) + + expect(transaction.action).to be_nil + end end end diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d925460f014c30929c81e7cf92eca02735dfa46 --- /dev/null +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +require 'tempfile' + +describe Gitlab::Middleware::Multipart do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + it 'opens top-level files' do + Tempfile.open('top-level') do |tempfile| + env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['file'] + expect(file).to be_a(::UploadedFile) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'rejects headers signed with the wrong secret' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse') + + expect { middleware.call(env) }.to raise_error(JWT::VerificationError) + end + + it 'rejects headers signed with the wrong issuer' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc') + + expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError) + end + + it 'opens files one level deep' do + Tempfile.open('one-level') do |tempfile| + in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } + env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['user']['avatar'] + expect(file).to be_a(::UploadedFile) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'opens files two levels deep' do + Tempfile.open('two-levels') do |tempfile| + in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } + env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['project']['milestone']['themesong'] + expect(file).to be_a(::UploadedFile) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + def post_env(rewritten_fields, params, secret, issuer) + token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256') + Rack::MockRequest.env_for( + '/', + method: 'post', + params: params, + described_class::RACK_ENV_KEY => token + ) + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 3cd9863ec6ae4a3092ffa2298d7e6ab0391f39b3..14ee386dba6c26fbb9d75f7651f01febf6bd560d 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -149,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.issues_count).to eq 3 end end + + describe 'notes search' do + it 'lists notes' do + project = create(:empty_project, :public) + note = create(:note, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).to include note + end + + it "doesn't list issue notes when access is restricted" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + + it "doesn't list merge_request notes when access is restricted" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + end end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index e5406fb2d33de0f9a28de0a6f14402f6e848958b..917c5c46db11e864e542c832d18ee8ca108fb059 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Redis do - let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } + include StubENV before(:each) { clear_raw_config } after(:each) { clear_raw_config } @@ -72,6 +72,20 @@ describe Gitlab::Redis do expect(url2).not_to end_with('foobar') end + + context 'when yml file with env variable' do + let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') } + + before do + stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379') + end + + it 'reads redis url from env variable' do + stub_const("#{described_class}::CONFIG_FILE", redis_config) + + expect(described_class.url).to eq 'redis://redishost:6379' + end + end end describe '._raw_config' do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c51b10bdc69b5870673b06cd998938dc6149ea6a..c78cd30157e5e8ed3264bf351a9965520c1d905d 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do describe 'file path regex' do it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } end + + describe 'environment slug regex' do + def be_matched + match(Gitlab::Regex.environment_slug_regex) + end + + it { expect('foo').to be_matched } + it { expect('foo-1').to be_matched } + + it { expect('FOO').not_to be_matched } + it { expect('foo/1').not_to be_matched } + it { expect('foo.1').not_to be_matched } + it { expect('foo*1').not_to be_matched } + it { expect('9foo').not_to be_matched } + it { expect('foo-').not_to be_matched } + end end diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..01d5acfc15b7ef244be147ba51fab4bfc92e2045 --- /dev/null +++ b/spec/lib/gitlab/routing_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Routing do + context 'when module is included' do + subject do + Class.new.include(described_class).new + end + + it 'makes it possible to access url helpers' do + expect(subject).to respond_to(:namespace_project_path) + end + end + + context 'when module is not included' do + subject do + Class.new.include(described_class.url_helpers).new + end + + it 'exposes url helpers module through a method' do + expect(subject).to respond_to(:namespace_project_path) + end + end +end diff --git a/spec/lib/gitlab/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serialize/ci/variables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7ea74da5252efec6b713024b29b42e0ffa279e3c --- /dev/null +++ b/spec/lib/gitlab/serialize/ci/variables_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::Serialize::Ci::Variables do + subject do + described_class.load(described_class.dump(object)) + end + + let(:object) do + [{ key: :key, value: 'value', public: true }, + { key: 'wee', value: 1, public: false }] + end + + it 'converts keys into strings' do + is_expected.to eq([ + { key: 'key', value: 'value', public: true }, + { key: 'wee', value: 1, public: false }]) + end +end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 0cdbab8754417a4a5d4494718186372f7d42d801..849edb094761ed49a7cc1f7cf660a2c5fd864007 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,16 +1,26 @@ require 'spec_helper' describe Gitlab::SQL::Union, lib: true do + let(:relation_1) { User.where(email: 'alice@example.com').select(:id) } + let(:relation_2) { User.where(email: 'bob@example.com').select(:id) } + + def to_sql(relation) + relation.reorder(nil).to_sql + end + describe '#to_sql' do it 'returns a String joining relations together using a UNION' do - rel1 = User.where(email: 'alice@example.com') - rel2 = User.where(email: 'bob@example.com') - union = described_class.new([rel1, rel2]) + union = described_class.new([relation_1, relation_2]) + + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") + end - sql1 = rel1.reorder(nil).to_sql - sql2 = rel2.reorder(nil).to_sql + it 'skips Model.none segements' do + empty_relation = User.none + union = described_class.new([empty_relation, relation_1, relation_2]) - expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index b5b685da904ffc787c10676a02c4f25f05a97a51..61da91dcbd344d79e722d3ca5f7cfe41feec95a0 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do end end + describe '.terminal_websocket' do + def terminal(ca_pem: nil) + out = { + subprotocols: ['foo'], + url: 'wss://example.com/terminal.ws', + headers: { 'Authorization' => ['Token x'] } + } + out[:ca_pem] = ca_pem if ca_pem + out + end + + def workhorse(ca_pem: nil) + out = { + 'Terminal' => { + 'Subprotocols' => ['foo'], + 'Url' => 'wss://example.com/terminal.ws', + 'Header' => { 'Authorization' => ['Token x'] } + } + } + out['Terminal']['CAPem'] = ca_pem if ca_pem + out + end + + context 'without ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal) } + + it { is_expected.to eq(workhorse) } + end + + context 'with ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal(ca_pem: "foo")) } + + it { is_expected.to eq(workhorse(ca_pem: "foo")) } + end + end + describe '.send_git_diff' do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..dc11a414717c2afe188884290c4af7b7b7e1a74c --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Mattermost::Client do + let(:user) { build(:user) } + + subject { described_class.new(user) } + + context 'JSON parse error' do + before do + Struct.new("Request", :body, :success?) + end + + it 'yields an error on malformed JSON' do + bad_json = Struct::Request.new("I'm not json", true) + expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) + end + + it 'shows a client error if the request was unsuccessful' do + bad_request = Struct::Request.new("true", false) + + expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) + end + end +end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ccf11008980a827f48a146fe31f22f92a87b9b1 --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Mattermost::Command do + let(:params) { { 'token' => 'token', team_id: 'abc' } } + + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#create' do + let(:params) do + { team_id: 'abc', + trigger: 'gitlab' + } + end + + subject { described_class.new(nil).create(params) } + + context 'for valid trigger word' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'returns a token' do + is_expected.to eq('token') + end + end + + context 'for error message' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') + end + end + end +end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..74d12e3718105f58afdc828b83a3196220d4c0fd --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe Mattermost::Session, type: :request do + let(:user) { create(:user) } + + let(:gitlab_url) { "http://gitlab.com" } + let(:mattermost_url) { "http://mattermost.com" } + + subject { described_class.new(user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + before do + described_class.base_uri(mattermost_url) + end + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create( + name: "GitLab Mattermost", + redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "state" } + let(:params) do + { response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: "#{mattermost_url}/signup/gitlab/complete", + state: state } + end + let(:location) do + "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" + end + + before do + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). + with(query: hash_including({ 'state' => state })). + to_return do |request| + post "/oauth/token", + client_id: doorkeeper.uid, + client_secret: doorkeeper.secret, + redirect_uri: params[:redirect_uri], + grant_type: 'authorization_code', + code: request.uri.query_values['code'] + + if response.status == 200 + { headers: { 'token' => 'thisworksnow' }, status: 202 } + end + end + + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). + to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) + end + + it 'can setup a session' do + subject.with_session do |session| + end + + expect(subject.token).not_to be_nil + end + + it 'returns the value of the block' do + result = subject.with_session do |session| + "value" + end + + expect(result).to eq("value") + end + end + end + + context 'with lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') + end + + it 'tries to obtain a lease' do + expect(subject).to receive(:lease_try_obtain) + expect(Gitlab::ExclusiveLease).to receive(:cancel) + + # Cannot setup a session, but we should still cancel the lease + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'without lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return(nil) + end + + it 'returns a NoSessionError error' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + end +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d14be6bcc2d277b017f9ff8ec48923633e33a71 --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Mattermost::Team do + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#all' do + subject { described_class.new(nil).all } + + context 'for valid request' do + let(:response) do + [{ + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false }] + end + + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: response.to_json + ) + end + + it 'returns a token' do + is_expected.to eq(response) + end + end + + context 'for error message' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.team.list.app_error', + message: 'Cannot list teams.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') + end + end + end +end diff --git a/spec/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..99dc41958188573a1cb15b3e99acecf667983456 --- /dev/null +++ b/spec/migrations/fill_authorized_projects_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb') + +describe FillAuthorizedProjects do + describe '#up' do + it 'schedules the jobs in batches' do + user1 = create(:user) + user2 = create(:user) + + expect(Sidekiq::Client).to receive(:push_bulk).with( + 'class' => 'AuthorizedProjectsWorker', + 'args' => [[user1.id], [user2.id]] + ) + + described_class.new.up + end + end +end diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8737e00eaeb5f9340aa92fc6be74fc06939bd552 --- /dev/null +++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb @@ -0,0 +1,57 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb') + +describe RemoveDotGitFromUsernames do + let(:user) { create(:user) } + let(:migration) { described_class.new } + + describe '#up' do + before do + update_namespace(user, 'test.git') + end + + it 'renames user with .git in username' do + migration.up + + expect(user.reload.username).to eq('test_git') + expect(user.namespace.reload.path).to eq('test_git') + expect(user.namespace.route.path).to eq('test_git') + end + end + + context 'when new path exists already' do + describe '#up' do + let(:user2) { create(:user) } + + before do + update_namespace(user, 'test.git') + update_namespace(user2, 'test_git') + + storages = { 'default' => 'tmp/tests/custom_repositories' } + + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(migration).to receive(:route_exists?).with('test_git').and_return(true) + allow(migration).to receive(:route_exists?).with('test_git1').and_return(false) + end + + it 'renames user with .git in username' do + migration.up + + expect(user.reload.username).to eq('test_git1') + expect(user.namespace.reload.path).to eq('test_git1') + expect(user.namespace.route.path).to eq('test_git1') + end + end + end + + def update_namespace(user, path) + namespace = user.namespace + namespace.path = path + namespace.save!(validate: false) + + user.username = path + user.save!(validate: false) + end +end diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4fb7ed36884ad6bce04de27c323bd5ae97b52fcc --- /dev/null +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -0,0 +1,47 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_project_names.rb') + +# This migration uses multiple threads, and thus different transactions. This +# means data created in this spec may not be visible to some threads. To work +# around this we use the TRUNCATE cleaning strategy. +describe RenameReservedProjectNames, truncate: true do + let(:migration) { described_class.new } + let!(:project) { create(:empty_project) } + + before do + project.path = 'projects' + project.save!(validate: false) + end + + describe '#up' do + context 'when project repository exists' do + before { project.create_repository } + + context 'when no exception is raised' do + it 'renames project with reserved names' do + migration.up + + expect(project.reload.path).to eq('projects0') + end + end + + context 'when exception is raised during rename' do + before do + allow(project).to receive(:rename_repo).and_raise(StandardError) + end + + it 'captures exception from project rename' do + expect { migration.up }.not_to raise_error + end + end + end + + context 'when project repository does not exist' do + it 'does not raise error' do + expect { migration.up }.not_to raise_error + end + end + end +end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb deleted file mode 100644 index e902d9ef73d28e4134d9311f286f0474a792261a..0000000000000000000000000000000000000000 --- a/spec/models/build_spec.rb +++ /dev/null @@ -1,1193 +0,0 @@ -require 'spec_helper' - -describe Ci::Build, models: true do - let(:project) { create(:project) } - - let(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: project.default_branch, - status: 'success') - end - - let(:build) { create(:ci_build, pipeline: pipeline) } - - it { is_expected.to validate_presence_of :ref } - - it { is_expected.to respond_to :trace_html } - - describe '#first_pending' do - let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } - let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } - subject { Ci::Build.first_pending } - - it { is_expected.to be_a(Ci::Build) } - it('returns with the first pending build') { is_expected.to eq(first) } - end - - describe '#create_from' do - before do - build.status = 'success' - build.save - end - let(:create_from_build) { Ci::Build.create_from build } - - it 'exists a pending task' do - expect(Ci::Build.pending.count(:all)).to eq 0 - create_from_build - expect(Ci::Build.pending.count(:all)).to be > 0 - end - end - - describe '#failed_but_allowed?' do - subject { build.failed_but_allowed? } - - context 'when build is not allowed to fail' do - before do - build.allow_failure = false - end - - context 'and build.status is success' do - before do - build.status = 'success' - end - - it { is_expected.to be_falsey } - end - - context 'and build.status is failed' do - before do - build.status = 'failed' - end - - it { is_expected.to be_falsey } - end - end - - context 'when build is allowed to fail' do - before do - build.allow_failure = true - end - - context 'and build.status is success' do - before do - build.status = 'success' - end - - it { is_expected.to be_falsey } - end - - context 'and build.status is failed' do - before do - build.status = 'failed' - end - - it { is_expected.to be_truthy } - end - end - end - - describe '#trace' do - it { expect(build.trace).to be_nil } - - context 'when build.trace contains text' do - let(:text) { 'example output' } - before do - build.trace = text - end - - it { expect(build.trace).to eq(text) } - end - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.update(trace: token) - build.project.update(runners_token: token) - end - - it { expect(build.trace).not_to include(token) } - it { expect(build.raw_trace).to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(trace: token) - build.update(token: token) - end - - it { expect(build.trace).not_to include(token) } - it { expect(build.raw_trace).to include(token) } - end - end - - describe '#raw_trace' do - subject { build.raw_trace } - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.project.update(runners_token: token) - build.update(trace: token) - end - - it { is_expected.not_to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(token: token) - build.update(trace: token) - end - - it { is_expected.not_to include(token) } - end - end - - context '#append_trace' do - subject { build.trace_html } - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.project.update(runners_token: token) - build.append_trace(token, 0) - end - - it { is_expected.not_to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(token: token) - build.append_trace(token, 0) - end - - it { is_expected.not_to include(token) } - end - end - - # TODO: build timeout - # describe :timeout do - # subject { build.timeout } - # - # it { is_expected.to eq(pipeline.project.timeout) } - # end - - describe '#options' do - let(:options) do - { - image: "ruby:2.1", - services: [ - "postgres" - ] - } - end - - subject { build.options } - it { is_expected.to eq(options) } - end - - # TODO: allow_git_fetch - # describe :allow_git_fetch do - # subject { build.allow_git_fetch } - # - # it { is_expected.to eq(project.allow_git_fetch) } - # end - - describe '#project' do - subject { build.project } - - it { is_expected.to eq(pipeline.project) } - end - - describe '#project_id' do - subject { build.project_id } - - it { is_expected.to eq(pipeline.project_id) } - end - - describe '#project_name' do - subject { build.project_name } - - it { is_expected.to eq(project.name) } - end - - describe '#extract_coverage' do - context 'valid content & regex' do - subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } - - it { is_expected.to eq(98.29) } - end - - context 'valid content & bad regex' do - subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } - - it { is_expected.to be_nil } - end - - context 'no coverage content & regex' do - subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } - - it { is_expected.to be_nil } - end - - context 'multiple results in content & regex' do - subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } - - it { is_expected.to eq(98.29) } - end - - context 'using a regex capture' do - subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } - - it { is_expected.to eq(65) } - end - end - - describe '#variables' do - let(:container_registry_enabled) { false } - let(:predefined_variables) do - [ - { key: 'CI', value: 'true', public: true }, - { key: 'GITLAB_CI', value: 'true', public: true }, - { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, - { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, - { key: 'CI_BUILD_REF', value: build.sha, public: true }, - { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, - { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, - { key: 'CI_BUILD_NAME', value: 'test', public: true }, - { key: 'CI_BUILD_STAGE', value: 'test', public: true }, - { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, - { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, - { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, - { key: 'CI_PROJECT_NAME', value: project.path, public: true }, - { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, - { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, - { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } - ] - end - - before do - stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') - end - - subject { build.variables } - - context 'returns variables' do - before do - build.yaml_variables = [] - end - - it { is_expected.to eq(predefined_variables) } - end - - context 'when build has user' do - let(:user) { create(:user, username: 'starter') } - let(:user_variables) do - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } - ] - end - - before do - build.update_attributes(user: user) - end - - it { user_variables.each { |v| is_expected.to include(v) } } - end - - context 'when build started manually' do - before do - build.update_attributes(when: :manual) - end - - let(:manual_variable) do - { key: 'CI_BUILD_MANUAL', value: 'true', public: true } - end - - it { is_expected.to include(manual_variable) } - end - - context 'when build is for tag' do - let(:tag_variable) do - { key: 'CI_BUILD_TAG', value: 'master', public: true } - end - - before do - build.update_attributes(tag: true) - end - - it { is_expected.to include(tag_variable) } - end - - context 'when secure variable is defined' do - let(:secure_variable) do - { key: 'SECRET_KEY', value: 'secret_value', public: false } - end - - before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') - end - - it { is_expected.to include(secure_variable) } - end - - context 'when build is for triggers' do - let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } - let(:user_trigger_variable) do - { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } - end - let(:predefined_trigger_variable) do - { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } - end - - before do - build.trigger_request = trigger_request - end - - it { is_expected.to include(user_trigger_variable) } - it { is_expected.to include(predefined_trigger_variable) } - end - - context 'when yaml_variables are undefined' do - before do - build.yaml_variables = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq(predefined_variables) } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq(predefined_variables) } - end - - context 'when config has variables' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - variables: { - KEY: 'value' - } - } - }) - end - let(:variables) do - [{ key: :KEY, value: 'value', public: true }] - end - - it { is_expected.to eq(predefined_variables + variables) } - end - end - end - - context 'when container registry is enabled' do - let(:container_registry_enabled) { true } - let(:ci_registry) do - { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } - end - let(:ci_registry_image) do - { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } - end - - context 'and is disabled for project' do - before do - project.update(container_registry_enabled: false) - end - - it { is_expected.to include(ci_registry) } - it { is_expected.not_to include(ci_registry_image) } - end - - context 'and is enabled for project' do - before do - project.update(container_registry_enabled: true) - end - - it { is_expected.to include(ci_registry) } - it { is_expected.to include(ci_registry_image) } - end - end - - context 'when runner is assigned to build' do - let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } - - before do - build.update(runner: runner) - end - - it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } - it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } - it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } - end - - context 'returns variables in valid order' do - before do - allow(build).to receive(:predefined_variables) { ['predefined'] } - allow(project).to receive(:predefined_variables) { ['project'] } - allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } - allow(build).to receive(:yaml_variables) { ['yaml'] } - allow(project).to receive(:secret_variables) { ['secret'] } - end - - it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } - end - end - - describe '#has_tags?' do - context 'when build has tags' do - subject { create(:ci_build, tag_list: ['tag']) } - it { is_expected.to have_tags } - end - - context 'when build does not have tags' do - subject { create(:ci_build, tag_list: []) } - it { is_expected.not_to have_tags } - end - end - - describe '#any_runners_online?' do - subject { build.any_runners_online? } - - context 'when no runners' do - it { is_expected.to be_falsey } - end - - context 'when there are runners' do - let(:runner) { create(:ci_runner) } - - before do - build.project.runners << runner - runner.update_attributes(contacted_at: 1.second.ago) - end - - it { is_expected.to be_truthy } - - it 'that is inactive' do - runner.update_attributes(active: false) - is_expected.to be_falsey - end - - it 'that is not online' do - runner.update_attributes(contacted_at: nil) - is_expected.to be_falsey - end - - it 'that cannot handle build' do - expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) - is_expected.to be_falsey - end - end - end - - describe '#stuck?' do - subject { build.stuck? } - - context "when commit_status.status is pending" do - before do - build.status = 'pending' - end - - it { is_expected.to be_truthy } - - context "and there are specific runner" do - let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } - - before do - build.project.runners << runner - runner.save - end - - it { is_expected.to be_falsey } - end - end - - %w[success failed canceled running].each do |state| - context "when commit_status.status is #{state}" do - before do - build.status = state - end - - it { is_expected.to be_falsey } - end - end - end - - describe '#artifacts?' do - subject { build.artifacts? } - - context 'artifacts archive does not exist' do - before do - build.update_attributes(artifacts_file: nil) - end - - it { is_expected.to be_falsy } - end - - context 'artifacts archive exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to be_truthy } - - context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } - it { is_expected.to be_falsy } - end - - context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } - it { is_expected.to be_truthy } - end - end - end - - describe '#artifacts_expired?' do - subject { build.artifacts_expired? } - - context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } - - it { is_expected.to be_truthy } - end - - context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } - - it { is_expected.to be_falsey } - end - end - - describe '#artifacts_metadata?' do - subject { build.artifacts_metadata? } - context 'artifacts metadata does not exist' do - it { is_expected.to be_falsy } - end - - context 'artifacts archive is a zip file and metadata exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to be_truthy } - end - end - describe '#repo_url' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - - subject { build.repo_url } - - it { is_expected.to be_a(String) } - it { is_expected.to end_with(".git") } - it { is_expected.to start_with(project.web_url[0..6]) } - it { is_expected.to include(build.token) } - it { is_expected.to include('gitlab-ci-token') } - it { is_expected.to include(project.web_url[7..-1]) } - end - - describe '#artifacts_expire_in' do - subject { build.artifacts_expire_in } - it { is_expected.to be_nil } - - context 'when artifacts_expire_at is specified' do - let(:expire_at) { Time.now + 7.days } - - before { build.artifacts_expire_at = expire_at } - - it { is_expected.to be_within(5).of(expire_at - Time.now) } - end - end - - describe '#artifacts_expire_in=' do - subject { build.artifacts_expire_in } - - it 'when assigning valid duration' do - build.artifacts_expire_in = '7 days' - - is_expected.to be_within(10).of(7.days.to_i) - end - - it 'when assigning invalid duration' do - expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError) - is_expected.to be_nil - end - - it 'when resseting value' do - build.artifacts_expire_in = nil - - is_expected.to be_nil - end - end - - describe '#keep_artifacts!' do - let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } - - it 'to reset expire_at' do - build.keep_artifacts! - - expect(build.artifacts_expire_at).to be_nil - end - end - - describe '#depends_on_builds' do - let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } - let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } - let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } - let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } - - it 'expects to have no dependents if this is first build' do - expect(build.depends_on_builds).to be_empty - end - - it 'expects to have one dependent if this is test' do - expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) - end - - it 'expects to have all builds from build and test stage if this is last' do - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) - end - - it 'expects to have retried builds instead the original ones' do - retried_rspec = Ci::Build.retry(rspec_test) - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) - end - end - - def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) - create(factory, source_project_id: pipeline.gl_project_id, - target_project_id: pipeline.gl_project_id, - source_branch: build.ref, - created_at: created_at) - end - - describe '#merge_request' do - context 'when a MR has a reference to the pipeline' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request) - - commits = [double(id: pipeline.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) - end - - it 'returns the single associated MR' do - expect(build.merge_request.id).to eq(@merge_request.id) - end - end - - context 'when there is not a MR referencing the pipeline' do - it 'returns nil' do - expect(build.merge_request).to be_nil - end - end - - context 'when more than one MR have a reference to the pipeline' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request) - @merge_request.close! - @merge_request2 = create_mr(build, pipeline, factory: :merge_request) - - commits = [double(id: pipeline.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) - allow(@merge_request2).to receive(:commits).and_return(commits) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) - end - - it 'returns the first MR' do - expect(build.merge_request.id).to eq(@merge_request.id) - end - end - - context 'when a Build is created after the MR' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) - pipeline2 = create(:ci_pipeline, project: project) - @build2 = create(:ci_build, pipeline: pipeline2) - - allow(@merge_request).to receive(:commits_sha). - and_return([pipeline.sha, pipeline2.sha]) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) - end - - it 'returns the current MR' do - expect(@build2.merge_request.id).to eq(@merge_request.id) - end - end - end - - describe 'build erasable' do - shared_examples 'erasable' do - it 'removes artifact file' do - expect(build.artifacts_file.exists?).to be_falsy - end - - it 'removes artifact metadata file' do - expect(build.artifacts_metadata.exists?).to be_falsy - end - - it 'erases build trace in trace file' do - expect(build.trace).to be_empty - end - - it 'sets erased to true' do - expect(build.erased?).to be true - end - - it 'sets erase date' do - expect(build.erased_at).not_to be_falsy - end - end - - context 'build is not erasable' do - let!(:build) { create(:ci_build) } - - describe '#erase' do - subject { build.erase } - - it { is_expected.to be false } - end - - describe '#erasable?' do - subject { build.erasable? } - it { is_expected.to eq false } - end - end - - context 'build is erasable' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } - - describe '#erase' do - before do - build.erase(erased_by: user) - end - - context 'erased by user' do - let!(:user) { create(:user, username: 'eraser') } - - include_examples 'erasable' - - it 'records user who erased a build' do - expect(build.erased_by).to eq user - end - end - - context 'erased by system' do - let(:user) { nil } - - include_examples 'erasable' - - it 'does not set user who erased a build' do - expect(build.erased_by).to be_nil - end - end - end - - describe '#erasable?' do - subject { build.erasable? } - it { is_expected.to be_truthy } - end - - describe '#erased?' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } - subject { build.erased? } - - context 'build has not been erased' do - it { is_expected.to be_falsey } - end - - context 'build has been erased' do - before do - build.erase - end - - it { is_expected.to be_truthy } - end - end - - context 'metadata and build trace are not available' do - let!(:build) { create(:ci_build, :success, :artifacts) } - - before do - build.remove_artifacts_metadata! - end - - describe '#erase' do - it 'does not raise error' do - expect { build.erase }.not_to raise_error - end - end - end - end - end - - describe '#commit' do - it 'returns commit pipeline has been created for' do - expect(build.commit).to eq project.commit - end - end - - describe '#when' do - subject { build.when } - - context 'when `when` is undefined' do - before do - build.when = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq('on_success') } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq('on_success') } - end - - context 'when config has `when`' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - when: 'always' - } - }) - end - - it { is_expected.to eq('always') } - end - end - end - end - - describe '#retryable?' do - context 'when build is running' do - before do - build.run! - end - - it { expect(build).not_to be_retryable } - end - - context 'when build is finished' do - before do - build.success! - end - - it { expect(build).to be_retryable } - end - end - - describe '#manual?' do - before do - build.update(when: value) - end - - subject { build.manual? } - - context 'when is set to manual' do - let(:value) { 'manual' } - - it { is_expected.to be_truthy } - end - - context 'when set to something else' do - let(:value) { 'something else' } - - it { is_expected.to be_falsey } - end - end - - describe '#other_actions' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - - subject { build.other_actions } - - it 'returns other actions' do - is_expected.to contain_exactly(other_build) - end - - context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build) } - - it 'does not return any of them' do - is_expected.not_to include(build, new_build) - end - end - - context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build) } - - it 'returns a retried build' do - is_expected.to contain_exactly(retried_build) - end - end - end - - describe '#play' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - - subject { build.play } - - it 'enqueues a build' do - is_expected.to be_pending - is_expected.to eq(build) - end - - context 'for successful build' do - before do - build.update(status: 'success') - end - - it 'creates a new build' do - is_expected.to be_pending - is_expected.not_to eq(build) - end - end - end - - describe '#when' do - subject { build.when } - - context 'when `when` is undefined' do - before do - build.when = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq('on_success') } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq('on_success') } - end - - context 'when config has when' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - when: 'always' - } - }) - end - - it { is_expected.to eq('always') } - end - end - end - end - - describe '#retryable?' do - context 'when build is running' do - before { build.run! } - - it 'returns false' do - expect(build).not_to be_retryable - end - end - - context 'when build is finished' do - before do - build.success! - end - - it 'returns true' do - expect(build).to be_retryable - end - end - end - - describe '#has_environment?' do - subject { build.has_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - it { is_expected.to be_truthy } - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#starts_environment?' do - subject { build.starts_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_truthy } - end - - context 'and start action is defined' do - before do - build.update(options: { environment: { action: 'start' } } ) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#stops_environment?' do - subject { build.stops_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_falsey } - end - - context 'and stop action is defined' do - before do - build.update(options: { environment: { action: 'stop' } } ) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#last_deployment' do - subject { build.last_deployment } - - context 'when multiple deployments are created' do - let!(:deployment1) { create(:deployment, deployable: build) } - let!(:deployment2) { create(:deployment, deployable: build) } - - it 'returns the latest one' do - is_expected.to eq(deployment2) - end - end - end - - describe '#outdated_deployment?' do - subject { build.outdated_deployment? } - - context 'when build succeeded' do - let(:build) { create(:ci_build, :success) } - let!(:deployment) { create(:deployment, deployable: build) } - - context 'current deployment is latest' do - it { is_expected.to be_falsey } - end - - context 'current deployment is not latest on environment' do - let!(:deployment2) { create(:deployment, environment: deployment.environment) } - - it { is_expected.to be_truthy } - end - end - - context 'when build failed' do - let(:build) { create(:ci_build, :failed) } - - it { is_expected.to be_falsey } - end - end - - describe '#expanded_environment_name' do - subject { build.expanded_environment_name } - - context 'when environment uses variables' do - let(:build) { create(:ci_build, ref: 'master', environment: 'review/$CI_BUILD_REF_NAME') } - - it { is_expected.to eq('review/master') } - end - end - - describe 'State transition: any => [:pending]' do - let(:build) { create(:ci_build, :created) } - - it 'queues BuildQueueWorker' do - expect(BuildQueueWorker).to receive(:perform_async).with(build.id) - - build.enqueue - end - end -end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a7e90c8a3819d3fc1a35b1a1609095c856530384..3309a7fff9f9b00ac8a85b2effbe3a6cb1bf2115 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,14 +1,962 @@ require 'spec_helper' -describe Ci::Build, models: true do - let(:build) { create(:ci_build) } +describe Ci::Build, :models do + let(:project) { create(:project) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:test_trace) { 'This is a test' } + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.id, + ref: project.default_branch, + status: 'success') + end + it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } + it { is_expected.to have_many(:deployments) } + it { is_expected.to validate_presence_of :ref } + it { is_expected.to respond_to :trace_html } + + describe '#any_runners_online?' do + subject { build.any_runners_online? } + + context 'when no runners' do + it { is_expected.to be_falsey } + end + + context 'when there are runners' do + let(:runner) { create(:ci_runner) } + + before do + build.project.runners << runner + runner.update_attributes(contacted_at: 1.second.ago) + end + + it { is_expected.to be_truthy } + + it 'that is inactive' do + runner.update_attributes(active: false) + is_expected.to be_falsey + end + + it 'that is not online' do + runner.update_attributes(contacted_at: nil) + is_expected.to be_falsey + end + + it 'that cannot handle build' do + expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) + is_expected.to be_falsey + end + end + end + + describe '#append_trace' do + subject { build.trace_html } + + context 'when build.trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + build.append_trace(token, 0) + end + + it { is_expected.not_to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + build.append_trace(token, 0) + end + + it { is_expected.not_to include(token) } + end + end + + describe '#artifacts?' do + subject { build.artifacts? } + + context 'artifacts archive does not exist' do + before do + build.update_attributes(artifacts_file: nil) + end + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + + context 'is expired' do + before { build.update(artifacts_expire_at: Time.now - 7.days) } + it { is_expected.to be_falsy } + end + + context 'is not expired' do + before { build.update(artifacts_expire_at: Time.now + 7.days) } + it { is_expected.to be_truthy } + end + end + end + + describe '#artifacts_expired?' do + subject { build.artifacts_expired? } + + context 'is expired' do + before { build.update(artifacts_expire_at: Time.now - 7.days) } + + it { is_expected.to be_truthy } + end + + context 'is not expired' do + before { build.update(artifacts_expire_at: Time.now + 7.days) } + + it { is_expected.to be_falsey } + end + end + + describe '#artifacts_metadata?' do + subject { build.artifacts_metadata? } + context 'artifacts metadata does not exist' do + it { is_expected.to be_falsy } + end + + context 'artifacts archive is a zip file and metadata exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + end + end + + describe '#artifacts_expire_in' do + subject { build.artifacts_expire_in } + it { is_expected.to be_nil } + + context 'when artifacts_expire_at is specified' do + let(:expire_at) { Time.now + 7.days } + + before { build.artifacts_expire_at = expire_at } + + it { is_expected.to be_within(5).of(expire_at - Time.now) } + end + end + + describe '#artifacts_expire_in=' do + subject { build.artifacts_expire_in } + + it 'when assigning valid duration' do + build.artifacts_expire_in = '7 days' + + is_expected.to be_within(10).of(7.days.to_i) + end + + it 'when assigning invalid duration' do + expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError) + is_expected.to be_nil + end + + it 'when resseting value' do + build.artifacts_expire_in = nil + + is_expected.to be_nil + end + end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(build.commit).to eq project.commit + end + end + + describe '#create_from' do + before do + build.status = 'success' + build.save + end + let(:create_from_build) { Ci::Build.create_from build } + + it 'exists a pending task' do + expect(Ci::Build.pending.count(:all)).to eq 0 + create_from_build + expect(Ci::Build.pending.count(:all)).to be > 0 + end + end + + describe '#depends_on_builds' do + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } + let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } + let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } + let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } + + it 'expects to have no dependents if this is first build' do + expect(build.depends_on_builds).to be_empty + end + + it 'expects to have one dependent if this is test' do + expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) + end + + it 'expects to have all builds from build and test stage if this is last' do + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) + end + + it 'expects to have retried builds instead the original ones' do + retried_rspec = Ci::Build.retry(rspec_test) + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + end + end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(build.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Build::Cancelable + end + end + + describe 'deployment' do + describe '#last_deployment' do + subject { build.last_deployment } + + context 'when multiple deployments are created' do + let!(:deployment1) { create(:deployment, deployable: build) } + let!(:deployment2) { create(:deployment, deployable: build) } + + it 'returns the latest one' do + is_expected.to eq(deployment2) + end + end + end + + describe '#outdated_deployment?' do + subject { build.outdated_deployment? } + + context 'when build succeeded' do + let(:build) { create(:ci_build, :success) } + let!(:deployment) { create(:deployment, deployable: build) } + + context 'current deployment is latest' do + it { is_expected.to be_falsey } + end + + context 'current deployment is not latest on environment' do + let!(:deployment2) { create(:deployment, environment: deployment.environment) } + + it { is_expected.to be_truthy } + end + end + + context 'when build failed' do + let(:build) { create(:ci_build, :failed) } + + it { is_expected.to be_falsey } + end + end + end + + describe 'environment' do + describe '#has_environment?' do + subject { build.has_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + it { is_expected.to be_truthy } + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#expanded_environment_name' do + subject { build.expanded_environment_name } + + context 'when environment uses $CI_BUILD_REF_NAME' do + let(:build) do + create(:ci_build, + ref: 'master', + environment: 'review/$CI_BUILD_REF_NAME') + end + + it { is_expected.to eq('review/master') } + end + + context 'when environment uses yaml_variables containing symbol keys' do + let(:build) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'review/$APP_HOST') + end + + it { is_expected.to eq('review/host') } + end + end + + describe '#starts_environment?' do + subject { build.starts_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_truthy } + end + + context 'and start action is defined' do + before do + build.update(options: { environment: { action: 'start' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#stops_environment?' do + subject { build.stops_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_falsey } + end + + context 'and stop action is defined' do + before do + build.update(options: { environment: { action: 'stop' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + end + + describe 'erasable build' do + shared_examples 'erasable' do + it 'removes artifact file' do + expect(build.artifacts_file.exists?).to be_falsy + end + + it 'removes artifact metadata file' do + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'erases build trace in trace file' do + expect(build.trace).to be_empty + end + + it 'sets erased to true' do + expect(build.erased?).to be true + end + + it 'sets erase date' do + expect(build.erased_at).not_to be_falsy + end + end + + context 'build is not erasable' do + let!(:build) { create(:ci_build) } + + describe '#erase' do + subject { build.erase } + + it { is_expected.to be false } + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq false } + end + end + + context 'build is erasable' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + + describe '#erase' do + before do + build.erase(erased_by: user) + end + + context 'erased by user' do + let!(:user) { create(:user, username: 'eraser') } + + include_examples 'erasable' + + it 'records user who erased a build' do + expect(build.erased_by).to eq user + end + end + + context 'erased by system' do + let(:user) { nil } + + include_examples 'erasable' + + it 'does not set user who erased a build' do + expect(build.erased_by).to be_nil + end + end + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to be_truthy } + end + + describe '#erased?' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + subject { build.erased? } + + context 'build has not been erased' do + it { is_expected.to be_falsey } + end + + context 'build has been erased' do + before do + build.erase + end + + it { is_expected.to be_truthy } + end + end + + context 'metadata and build trace are not available' do + let!(:build) { create(:ci_build, :success, :artifacts) } + + before do + build.remove_artifacts_metadata! + end + + describe '#erase' do + it 'does not raise error' do + expect { build.erase }.not_to raise_error + end + end + end + end + end + + describe '#extract_coverage' do + context 'valid content & regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'valid content & bad regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } + + it { is_expected.to be_nil } + end + + context 'no coverage content & regex' do + subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } + + it { is_expected.to be_nil } + end + + context 'multiple results in content & regex' do + subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'using a regex capture' do + subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } + + it { is_expected.to eq(65) } + end + end + + describe '#first_pending' do + let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } + let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } + subject { Ci::Build.first_pending } + + it { is_expected.to be_a(Ci::Build) } + it('returns with the first pending build') { is_expected.to eq(first) } + end + + describe '#failed_but_allowed?' do + subject { build.failed_but_allowed? } + + context 'when build is not allowed to fail' do + before do + build.allow_failure = false + end + + context 'and build.status is success' do + before do + build.status = 'success' + end + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before do + build.status = 'failed' + end + + it { is_expected.to be_falsey } + end + end + + context 'when build is allowed to fail' do + before do + build.allow_failure = true + end + + context 'and build.status is success' do + before do + build.status = 'success' + end + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before do + build.status = 'failed' + end + + it { is_expected.to be_truthy } + end + end + end + + describe 'flags' do + describe '#cancelable?' do + subject { build } + + context 'when build is cancelable' do + context 'when build is pending' do + it { is_expected.to be_cancelable } + end + + context 'when build is running' do + before do + build.run! + end + + it { is_expected.to be_cancelable } + end + end + + context 'when build is not cancelable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.not_to be_cancelable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.not_to be_cancelable } + end + end + end + + describe '#retryable?' do + subject { build } + + context 'when build is retryable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.to be_retryable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.to be_retryable } + end + + context 'when build is canceled' do + before do + build.cancel! + end + + it { is_expected.to be_retryable } + end + end + + context 'when build is not retryable' do + context 'when build is running' do + before do + build.run! + end + + it { is_expected.not_to be_retryable } + end + + context 'when build is skipped' do + before do + build.skip! + end + + it { is_expected.not_to be_retryable } + end + end + end + + describe '#manual?' do + before do + build.update(when: value) + end + + subject { build.manual? } + + context 'when is set to manual' do + let(:value) { 'manual' } + + it { is_expected.to be_truthy } + end + + context 'when set to something else' do + let(:value) { 'something else' } + + it { is_expected.to be_falsey } + end + end + end + + describe '#has_tags?' do + context 'when build has tags' do + subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when build does not have tags' do + subject { create(:ci_build, tag_list: []) } + it { is_expected.not_to have_tags } + end + end + + describe '#keep_artifacts!' do + let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } + + it 'to reset expire_at' do + build.keep_artifacts! + + expect(build.artifacts_expire_at).to be_nil + end + end + + describe '#merge_request' do + def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) + create(factory, source_project_id: pipeline.gl_project_id, + target_project_id: pipeline.gl_project_id, + source_branch: build.ref, + created_at: created_at) + end + + context 'when a MR has a reference to the pipeline' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request) + + commits = [double(id: pipeline.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the single associated MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when there is not a MR referencing the pipeline' do + it 'returns nil' do + expect(build.merge_request).to be_nil + end + end + + context 'when more than one MR have a reference to the pipeline' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request) + @merge_request.close! + @merge_request2 = create_mr(build, pipeline, factory: :merge_request) + + commits = [double(id: pipeline.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(@merge_request2).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) + end + + it 'returns the first MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when a Build is created after the MR' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) + pipeline2 = create(:ci_pipeline, project: project) + @build2 = create(:ci_build, pipeline: pipeline2) + + allow(@merge_request).to receive(:commits_sha). + and_return([pipeline.sha, pipeline2.sha]) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the current MR' do + expect(@build2.merge_request.id).to eq(@merge_request.id) + end + end + end + + describe '#options' do + let(:options) do + { + image: "ruby:2.1", + services: [ + "postgres" + ] + } + end - it { is_expected.to have_many(:deployments) } + it 'contains options' do + expect(build.options).to eq(options) + end + end + + describe '#other_actions' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } + + subject { build.other_actions } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + + context 'when build is retried' do + let!(:new_build) { Ci::Build.retry(build) } + + it 'does not return any of them' do + is_expected.not_to include(build, new_build) + end + end + + context 'when other build is retried' do + let!(:retried_build) { Ci::Build.retry(other_build) } + + it 'returns a retried build' do + is_expected.to contain_exactly(retried_build) + end + end + end + + describe '#persisted_environment' do + before do + @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'referenced literally' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it { is_expected.to eq(@environment) } + end + + context 'referenced with a variable' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + + it { is_expected.to eq(@environment) } + end + end + + describe '#play' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { build.play } + + it 'enqueues a build' do + is_expected.to be_pending + is_expected.to eq(build) + end + + context 'for successful build' do + before do + build.update(status: 'success') + end + + it 'creates a new build' do + is_expected.to be_pending + is_expected.not_to eq(build) + end + end + end + + describe 'project settings' do + describe '#timeout' do + it 'returns project timeout configuration' do + expect(build.timeout).to eq(project.build_timeout) + end + end + + describe '#allow_git_fetch' do + it 'return project allow_git_fetch configuration' do + expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch) + end + end + end + + describe '#project' do + subject { build.project } + + it { is_expected.to eq(pipeline.project) } + end + + describe '#project_id' do + subject { build.project_id } + + it { is_expected.to eq(pipeline.project_id) } + end + + describe '#project_name' do + subject { build.project_name } + + it { is_expected.to eq(project.name) } + end + + describe '#raw_trace' do + subject { build.raw_trace } + + context 'when build.trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + build.update(trace: token) + end + + it { is_expected.not_to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + build.update(trace: token) + end + + it { is_expected.not_to include(token) } + end + end + + describe '#ref_slug' do + { + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + }.each do |ref, slug| + it "transforms #{ref} to #{slug}" do + build.ref = ref + + expect(build.ref_slug).to eq(slug) + end + end + end + + describe '#repo_url' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject { build.repo_url } + + it { is_expected.to be_a(String) } + it { is_expected.to end_with(".git") } + it { is_expected.to start_with(project.web_url[0..6]) } + it { is_expected.to include(build.token) } + it { is_expected.to include('gitlab-ci-token') } + it { is_expected.to include(project.web_url[7..-1]) } + end + + describe '#stuck?' do + subject { build.stuck? } + + context "when commit_status.status is pending" do + before do + build.status = 'pending' + end + + it { is_expected.to be_truthy } + + context "and there are specific runner" do + let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } + + before do + build.project.runners << runner + runner.save + end + + it { is_expected.to be_falsey } + end + end + + %w[success failed canceled running].each do |state| + context "when commit_status.status is #{state}" do + before do + build.status = state + end + + it { is_expected.to be_falsey } + end + end + end describe '#trace' do it 'obfuscates project runners token' do @@ -24,6 +972,63 @@ describe Ci::Build, models: true do expect(build.trace).to eq(test_trace) end + + context 'when build does not have trace' do + it 'is is empty' do + expect(build.trace).to be_nil + end + end + + context 'when trace contains text' do + let(:text) { 'example output' } + before do + build.trace = text + end + + it { expect(build.trace).to eq(text) } + end + + context 'when trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.update(trace: token) + build.project.update(runners_token: token) + end + + it { expect(build.trace).not_to include(token) } + it { expect(build.raw_trace).to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(trace: token) + build.update(token: token) + end + + it { expect(build.trace).not_to include(token) } + it { expect(build.raw_trace).to include(token) } + end + end + + describe '#has_expiring_artifacts?' do + context 'when artifacts have expiration date set' do + before { build.update(artifacts_expire_at: 1.day.from_now) } + + it 'has expiring artifacts' do + expect(build).to have_expiring_artifacts + end + end + + context 'when artifacts do not have expiration date set' do + before { build.update(artifacts_expire_at: nil) } + + it 'does not have expiring artifacts' do + expect(build).not_to have_expiring_artifacts + end + end end describe '#has_trace_file?' do @@ -85,4 +1090,315 @@ describe Ci::Build, models: true do it { expect(build.trace_file_path).to eq(build.old_path_to_trace) } end end + + describe '#update_project_statistics' do + let!(:build) { create(:ci_build, artifacts_size: 23) } + + it 'updates project statistics when the artifact size changes' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(build.project_id, [], [:build_artifacts_size]) + + build.artifacts_size = 42 + build.save! + end + + it 'does not update project statistics when the artifact size stays the same' do + expect(ProjectCacheWorker).not_to receive(:perform_async) + + build.name = 'changed' + build.save! + end + + it 'updates project statistics when the build is destroyed' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(build.project_id, [], [:build_artifacts_size]) + + build.destroy + end + end + + describe '#when' do + subject { build.when } + + context 'when `when` is undefined' do + before do + build.when = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'when config is not found' do + let(:config) { nil } + + it { is_expected.to eq('on_success') } + end + + context 'when config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq('on_success') } + end + + context 'when config has `when`' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + when: 'always' + } + }) + end + + it { is_expected.to eq('always') } + end + end + end + end + + describe '#variables' do + let(:container_registry_enabled) { false } + let(:predefined_variables) do + [ + { key: 'CI', value: 'true', public: true }, + { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, + { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, + { key: 'CI_BUILD_REF', value: build.sha, public: true }, + { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, + { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, + { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, + { key: 'CI_BUILD_NAME', value: 'test', public: true }, + { key: 'CI_BUILD_STAGE', value: 'test', public: true }, + { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, + { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, + { key: 'CI_PROJECT_NAME', value: project.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, + { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } + ] + end + + before do + stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') + end + + subject { build.variables } + + context 'returns variables' do + before do + build.yaml_variables = [] + end + + it { is_expected.to eq(predefined_variables) } + end + + context 'when build has user' do + let(:user) { create(:user, username: 'starter') } + let(:user_variables) do + [ + { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } + ] + end + + before do + build.update_attributes(user: user) + end + + it { user_variables.each { |v| is_expected.to include(v) } } + end + + context 'when build has an environment' do + before do + build.update(environment: 'production') + create(:environment, project: build.project, name: 'production', slug: 'prod-slug') + end + + let(:environment_variables) do + [ + { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } + ] + end + + it { environment_variables.each { |v| is_expected.to include(v) } } + end + + context 'when build started manually' do + before do + build.update_attributes(when: :manual) + end + + let(:manual_variable) do + { key: 'CI_BUILD_MANUAL', value: 'true', public: true } + end + + it { is_expected.to include(manual_variable) } + end + + context 'when build is for tag' do + let(:tag_variable) do + { key: 'CI_BUILD_TAG', value: 'master', public: true } + end + + before do + build.update_attributes(tag: true) + end + + it { is_expected.to include(tag_variable) } + end + + context 'when secure variable is defined' do + let(:secure_variable) do + { key: 'SECRET_KEY', value: 'secret_value', public: false } + end + + before do + build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + it { is_expected.to include(secure_variable) } + end + + context 'when build is for triggers' do + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } + let(:user_trigger_variable) do + { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } + end + let(:predefined_trigger_variable) do + { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } + end + + before do + build.trigger_request = trigger_request + end + + it { is_expected.to include(user_trigger_variable) } + it { is_expected.to include(predefined_trigger_variable) } + end + + context 'when yaml_variables are undefined' do + before do + build.yaml_variables = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'when config is not found' do + let(:config) { nil } + + it { is_expected.to eq(predefined_variables) } + end + + context 'when config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq(predefined_variables) } + end + + context 'when config has variables' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + variables: { + KEY: 'value' + } + } + }) + end + let(:variables) do + [{ key: 'KEY', value: 'value', public: true }] + end + + it { is_expected.to eq(predefined_variables + variables) } + end + end + end + + context 'when container registry is enabled' do + let(:container_registry_enabled) { true } + let(:ci_registry) do + { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } + end + let(:ci_registry_image) do + { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } + end + + context 'and is disabled for project' do + before do + project.update(container_registry_enabled: false) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.not_to include(ci_registry_image) } + end + + context 'and is enabled for project' do + before do + project.update(container_registry_enabled: true) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.to include(ci_registry_image) } + end + end + + context 'when runner is assigned to build' do + let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } + + before do + build.update(runner: runner) + end + + it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } + end + + context 'when build is for a deployment' do + let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } + + before do + build.environment = 'production' + allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + end + + it { is_expected.to include(deployment_variable) } + end + + context 'returns variables in valid order' do + before do + allow(build).to receive(:predefined_variables) { ['predefined'] } + allow(project).to receive(:predefined_variables) { ['project'] } + allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } + allow(build).to receive(:yaml_variables) { ['yaml'] } + allow(project).to receive(:secret_variables) { ['secret'] } + end + + it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8158e71dd55aa92572efbafc75f0e77fe53701fa..d1aee27057abe3cd2a100d6111301abd4fa63aa6 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -175,6 +175,30 @@ describe Ci::Pipeline, models: true do end end + describe '#stage' do + subject { pipeline.stage('test') } + + context 'with status in stage' do + before do + create(:commit_status, pipeline: pipeline, stage: 'test') + end + + it { expect(subject).to be_a Ci::Stage } + it { expect(subject.name).to eq 'test' } + it { expect(subject.statuses).not_to be_empty } + end + + context 'without status in stage' do + before do + create(:commit_status, pipeline: pipeline, stage: 'build') + end + + it 'return stage object' do + is_expected.to be_nil + end + end + end + describe 'state machine' do let(:current) { Time.now.change(usec: 0) } let(:build) { create_build('build1', 0) } @@ -381,6 +405,78 @@ describe Ci::Pipeline, models: true do end end + shared_context 'with some outdated pipelines' do + before do + create_pipeline(:canceled, 'ref', 'A') + create_pipeline(:success, 'ref', 'A') + create_pipeline(:failed, 'ref', 'B') + create_pipeline(:skipped, 'feature', 'C') + end + + def create_pipeline(status, ref, sha) + create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + end + end + + describe '.latest' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:pipelines) { described_class.latest.all } + + it 'returns the latest pipeline for the same ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed', 'skipped') + end + end + + context 'when ref is specified' do + let(:pipelines) { described_class.latest('ref').all } + + it 'returns the latest pipeline for ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed') + end + end + end + + describe '.latest_status' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:latest_status) { described_class.latest_status } + + it 'returns the latest status for the same ref and different sha' do + expect(latest_status).to eq(described_class.latest.status) + expect(latest_status).to eq('failed') + end + end + + context 'when ref is specified' do + let(:latest_status) { described_class.latest_status('ref') } + + it 'returns the latest status for ref and different sha' do + expect(latest_status).to eq(described_class.latest_status('ref')) + expect(latest_status).to eq('failed') + end + end + end + + describe '.latest_successful_for' do + include_context 'with some outdated pipelines' + + let!(:latest_successful_pipeline) do + create_pipeline(:success, 'ref', 'D') + end + + it 'returns the latest successful pipeline' do + expect(described_class.latest_successful_for('ref')). + to eq(latest_successful_pipeline) + end + end + describe '#status' do let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } @@ -442,11 +538,15 @@ describe Ci::Pipeline, models: true do end describe '#detailed_status' do + let(:user) { create(:user) } + + subject { pipeline.detailed_status(user) } + context 'when pipeline is created' do let(:pipeline) { create(:ci_pipeline, status: :created) } it 'returns detailed status for created pipeline' do - expect(pipeline.detailed_status.text).to eq 'created' + expect(subject.text).to eq 'created' end end @@ -454,7 +554,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :pending) } it 'returns detailed status for pending pipeline' do - expect(pipeline.detailed_status.text).to eq 'pending' + expect(subject.text).to eq 'pending' end end @@ -462,7 +562,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :running) } it 'returns detailed status for running pipeline' do - expect(pipeline.detailed_status.text).to eq 'running' + expect(subject.text).to eq 'running' end end @@ -470,7 +570,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :success) } it 'returns detailed status for successful pipeline' do - expect(pipeline.detailed_status.text).to eq 'passed' + expect(subject.text).to eq 'passed' end end @@ -478,7 +578,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :failed) } it 'returns detailed status for failed pipeline' do - expect(pipeline.detailed_status.text).to eq 'failed' + expect(subject.text).to eq 'failed' end end @@ -486,7 +586,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :canceled) } it 'returns detailed status for canceled pipeline' do - expect(pipeline.detailed_status.text).to eq 'canceled' + expect(subject.text).to eq 'canceled' end end @@ -494,7 +594,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :skipped) } it 'returns detailed status for skipped pipeline' do - expect(pipeline.detailed_status.text).to eq 'skipped' + expect(subject.text).to eq 'skipped' end end @@ -506,7 +606,7 @@ describe Ci::Pipeline, models: true do end it 'retruns detailed status for successful pipeline with warnings' do - expect(pipeline.detailed_status.label).to eq 'passed with warnings' + expect(subject.label).to eq 'passed with warnings' end end end @@ -788,6 +888,48 @@ describe Ci::Pipeline, models: true do end end + describe '#stuck?' do + before do + create(:ci_build, :pending, pipeline: pipeline) + end + + context 'when pipeline is stuck' do + it 'is stuck' do + expect(pipeline).to be_stuck + end + end + + context 'when pipeline is not stuck' do + before { create(:ci_runner, :shared, :online) } + + it 'is not stuck' do + expect(pipeline).not_to be_stuck + end + end + end + + describe '#has_yaml_errors?' do + context 'when pipeline has errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: nil }) + end + + it 'contains yaml errors' do + expect(pipeline).to have_yaml_errors + end + end + + context 'when pipeline does not have errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { script: 'rake test' } }) + end + + it 'does not containyaml errors' do + expect(pipeline).not_to have_yaml_errors + end + end + end + describe 'notifications when pipeline success or failed' do let(:project) { create(:project) } diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index f232761dba282b636ff4f8c75d900a9c02cb00f7..742bedb37e49620f9ed9868e8754a5c994350787 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -28,6 +28,19 @@ describe Ci::Stage, models: true do end end + describe '#statuses_count' do + before do + create_job(:ci_build) + create_job(:ci_build, stage: 'other stage') + end + + subject { stage.statuses_count } + + it "counts statuses only from current stage" do + is_expected.to eq(1) + end + end + describe '#builds' do let!(:stage_build) { create_job(:ci_build) } let!(:commit_status) { create_job(:commit_status) } @@ -68,7 +81,9 @@ describe Ci::Stage, models: true do end describe '#detailed_status' do - subject { stage.detailed_status } + let(:user) { create(:user) } + + subject { stage.detailed_status(user) } context 'when build is created' do let!(:stage_build) { create_job(:ci_build, status: :created) } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0935fc0561cbfe22d3f39b91d11acc46fd00ea88..74b50d2908dcf0c9ad94b26f67f0de87892ffae1 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -213,23 +213,19 @@ eos end describe '#status' do - context 'without arguments for compound status' do - shared_examples 'giving the status from pipeline' do - it do - expect(commit.status).to eq(Ci::Pipeline.status) - end - end - - context 'with pipelines' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: commit.sha) + context 'without ref argument' do + before do + %w[success failed created pending].each do |status| + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: status) end - - it_behaves_like 'giving the status from pipeline' end - context 'without pipelines' do - it_behaves_like 'giving the status from pipeline' + it 'gives compound status from latest pipelines' do + expect(commit.status).to eq(Ci::Pipeline.latest_status) + expect(commit.status).to eq('pending') end end @@ -255,8 +251,9 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives compound status if ref is nil' do - expect(commit.status(nil)).to eq(commit.status) + it 'gives compound status from latest pipelines if ref is nil' do + expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) + expect(commit.status(nil)).to eq('failed') end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 1ec08c2a9d0ef72b3a01bec979ebb512945dffdf..64ea607eb95a334f2d5e5a71aef55b219adfaeca 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -234,4 +234,32 @@ describe CommitStatus, models: true do end end end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(commit_status.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Success + end + end + + describe '#sortable_name' do + tests = { + 'karma' => ['karma'], + 'karma 0 20' => ['karma ', 0, ' ', 20], + 'karma 10 20' => ['karma ', 10, ' ', 20], + 'karma 50:100' => ['karma ', 50, ':', 100], + 'karma 1.10' => ['karma ', 1, '.', 10], + 'karma 1.5.1' => ['karma ', 1, '.', 5, '.', 1], + 'karma 1 a' => ['karma ', 1, ' a'] + } + + tests.each do |name, sortable_name| + it "'#{name}' sorts as '#{sortable_name}'" do + commit_status.name = name + expect(commit_status.sortable_name).to eq(sortable_name) + end + end + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 4fa06a8c60ac97d6d817662eb1880e8d1cfc4a42..d7d31892e126b90534124d1d58ac2875015f4442 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -44,6 +44,45 @@ describe Issue, "Issuable" do it { expect(described_class).to respond_to(:assigned) } end + describe "before_save" do + describe "#update_cache_counts" do + context "when previous assignee exists" do + before do + assignee = create(:user) + issue.project.team << [assignee, :developer] + issue.update(assignee: assignee) + end + + it "updates cache counts for new assignee" do + user = create(:user) + + expect(user).to receive(:update_cache_counts) + + issue.update(assignee: user) + end + + it "updates cache counts for previous assignee" do + old_assignee = issue.assignee + allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee) + + expect(old_assignee).to receive(:update_cache_counts) + + issue.update(assignee: nil) + end + end + + context "when previous assignee does not exist" do + before{ issue.update(assignee: nil) } + + it "updates cache count for the new assignee" do + expect_any_instance_of(User).to receive(:update_cache_counts) + + issue.update(assignee: user) + end + end + end + end + describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } @@ -369,4 +408,42 @@ describe Issue, "Issuable" do expect(issue.assignee_or_author?(user)).to eq(false) end end + + describe '#spend_time' do + let(:user) { create(:user) } + let(:issue) { create(:issue) } + + def spend_time(seconds) + issue.spend_time(duration: seconds, user: user) + issue.save! + end + + context 'adding time' do + it 'should update the total time spent' do + spend_time(1800) + + expect(issue.total_time_spent).to eq(1800) + end + end + + context 'substracting time' do + before do + spend_time(1800) + end + + it 'should update the total time spent' do + spend_time(-900) + + expect(issue.total_time_spent).to eq(900) + end + + context 'when time to substract exceeds the total time spent' do + it 'raise a validation error' do + expect do + spend_time(-3600) + end.to raise_error(ActiveRecord::RecordInvalid) + end + end + end + end end diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a0765a264cfcd2b744b79e97c19ddd852301b900 --- /dev/null +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe ReactiveCaching, caching: true do + include ReactiveCachingHelpers + + class CacheTest + include ReactiveCaching + + self.reactive_cache_key = ->(thing) { ["foo", thing.id] } + + self.reactive_cache_lifetime = 5.minutes + self.reactive_cache_refresh_interval = 15.seconds + + attr_reader :id + + def initialize(id, &blk) + @id = id + @calculator = blk + end + + def calculate_reactive_cache + @calculator.call + end + + def result + with_reactive_cache do |data| + data / 2 + end + end + end + + let(:now) { Time.now.utc } + + around(:each) do |example| + Timecop.freeze(now) { example.run } + end + + let(:calculation) { -> { 2 + 2 } } + let(:cache_key) { "foo:666" } + let(:instance) { CacheTest.new(666, &calculation) } + + describe '#with_reactive_cache' do + before { stub_reactive_cache } + subject(:go!) { instance.result } + + context 'when cache is empty' do + it { is_expected.to be_nil } + + it 'queues a background worker' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) + + go! + end + + it 'updates the cache lifespan' do + go! + + expect(reactive_cache_alive?(instance)).to be_truthy + end + end + + context 'when the cache is full' do + before { stub_reactive_cache(instance, 4) } + + it { is_expected.to eq(2) } + + context 'and expired' do + before { invalidate_reactive_cache(instance) } + it { is_expected.to be_nil } + end + end + end + + describe '#clear_reactive_cache!' do + before do + stub_reactive_cache(instance, 4) + instance.clear_reactive_cache! + end + + it { expect(instance.result).to be_nil } + end + + describe '#exclusively_update_reactive_cache!' do + subject(:go!) { instance.exclusively_update_reactive_cache! } + + context 'when the lease is free and lifetime is not exceeded' do + before { stub_reactive_cache(instance, "preexisting") } + + it 'takes and releases the lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000") + expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000") + + go! + end + + it 'caches the result of #calculate_reactive_cache' do + go! + + expect(read_reactive_cache(instance)).to eq(calculation.call) + end + + it "enqueues a repeat worker" do + expect_reactive_cache_update_queued(instance) + + go! + end + + context 'and #calculate_reactive_cache raises an exception' do + before { stub_reactive_cache(instance, "preexisting") } + let(:calculation) { -> { raise "foo"} } + + it 'leaves the cache untouched' do + expect { go! }.to raise_error("foo") + expect(read_reactive_cache(instance)).to eq("preexisting") + end + + it 'enqueues a repeat worker' do + expect_reactive_cache_update_queued(instance) + + expect { go! }.to raise_error("foo") + end + end + end + + context 'when lifetime is exceeded' do + it 'skips the calculation' do + expect(instance).to receive(:calculate_reactive_cache).never + + go! + end + end + + context 'when the lease is already taken' do + before do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil) + end + + it 'skips the calculation' do + expect(instance).to receive(:calculate_reactive_cache).never + + go! + end + end + end +end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index eb64f3d0c83291796004dcf70468e546bd291bed..4b0bfa43abf0c27b7d617c34dab146d011927c1d 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } + it { is_expected.to respond_to("set_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } end end @@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end end + describe 'setting new token' do + subject { described_class.new.send("set_#{token_field}", '0123456789') } + + it { is_expected.to eq '0123456789' } + end + describe 'multiple token fields' do before do described_class.send(:add_authentication_token_field, :yet_another_token) diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7771785ead3e9182f2145765029a23f18068c455..70f985afefb1bba0292186f4c87237a0a1d2890b 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( @@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) context.deploy_master @@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do deploy_master end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end @@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) end) @@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5ed3d37f2fbfb18601d2c271d88e1612a1f38d4e..e4b6a8f45180753656cb040af935659436b4e975 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :issue, @@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do merge_merge_requests_closing_issue(issue) end - expect(subject.issue).to be_nil + expect(subject[:issue].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index baf3e3241a128cfac97440ef60cf3ac27b84a029..dc5b04852d668c8a40ebd625ca03ad53d2683977 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :plan, @@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do create_merge_request_closing_issue(issue, source_branch: branch_name) merge_merge_requests_closing_issue(issue) - expect(subject.issue).to be_nil + expect(subject[:issue].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 21b9c6e71507a7e93fcd63c192bcd2dc346ea938..5e99188f318e31dd0e179da0c244b377d4ca2a51 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :production, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master(environment: 'staging') end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 158621d59a43ef0edd38fca5e6230ea12cd554a5..45baa5f700644c70453029050ad6ce70fbad0089 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :review, @@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) end - expect(subject.review).to be_nil + expect(subject[:review].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index dad653964b74cd59b16aae8b5c8426ace1f2b34f..77625aad5806495bdf5f046b13a41815ae09314e 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :staging, @@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end @@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master(environment: 'staging') end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 2313724e8f38274aba2301bbb98b95d2dc46ab5c..27a117d2d76ba832eb0d11219ebe2218ba47224e 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :test, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do pipeline.succeed! end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 93623e8e99b851581eab10537e3c91e63fd7f24d..8ef8218cf74b0a3b8751ce9aeaeba6c373034c06 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -1,8 +1,22 @@ require 'spec_helper' describe DeployKey, models: true do + include EmailHelpers + describe "Associations" do it { is_expected.to have_many(:deploy_keys_projects) } it { is_expected.to have_many(:projects) } end + + describe 'notification' do + let(:user) { create(:user) } + + it 'does not send a notification' do + perform_enqueued_jobs do + create(:deploy_key, user: user) + end + + should_not_email(user) + end + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c8170164898640e2a49bc42729580745f1168bc8..96efe1696c31905ece1c996ad0c653dbc90bf22b 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Environment, models: true do - let(:environment) { create(:environment) } + let(:project) { create(:empty_project) } + subject(:environment) { create(:environment, project: project) } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } @@ -15,15 +16,11 @@ describe Environment, models: true do it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } - it { is_expected.to validate_length_of(:external_url).is_at_most(255) } - - # To circumvent a not null violation of the name column: - # https://github.com/thoughtbot/shoulda-matchers/issues/336 - it 'validates uniqueness of :external_url' do - create(:environment) + it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) } + it { is_expected.to validate_length_of(:slug).is_at_most(24) } - is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) - end + it { is_expected.to validate_length_of(:external_url).is_at_most(255) } + it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } describe '#nullify_external_url' do it 'replaces a blank url with nil' do @@ -35,6 +32,8 @@ describe Environment, models: true do end describe '#includes_commit?' do + let(:project) { create(:project) } + context 'without a last deployment' do it "returns false" do expect(environment.includes_commit?('HEAD')).to be false @@ -42,9 +41,6 @@ describe Environment, models: true do end context 'with a last deployment' do - let(:project) { create(:project) } - let(:environment) { create(:environment, project: project) } - let!(:deployment) do create(:deployment, environment: environment, sha: project.commit('master').id) end @@ -67,9 +63,25 @@ describe Environment, models: true do end end + describe '#update_merge_request_metrics?' do + { 'production' => true, + 'production/eu' => true, + 'production/www.gitlab.com' => true, + 'productioneu' => false, + 'Production' => false, + 'Production/eu' => false, + 'test-production' => false + }.each do |name, expected_value| + it "returns #{expected_value} for #{name}" do + env = create(:environment, name: name) + + expect(env.update_merge_request_metrics?).to eq(expected_value) + end + end + end + describe '#first_deployment_for' do let(:project) { create(:project) } - let!(:environment) { create(:environment, project: project) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } let(:head_commit) { project.commit } @@ -199,4 +211,89 @@ describe Environment, models: true do expect(environment.actions_for('review/master')).to contain_exactly(review_action) end end + + describe '#has_terminals?' do + subject { environment.has_terminals? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:kubernetes_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a deployment service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:kubernetes_project) } + before { environment.stop } + it { is_expected.to be_falsy } + end + end + + describe '#terminals' do + let(:project) { create(:kubernetes_project) } + subject { environment.terminals } + + context 'when the environment has terminals' do + before { allow(environment).to receive(:has_terminals?).and_return(true) } + + it 'returns the terminals from the deployment service' do + expect(project.deployment_service). + to receive(:terminals).with(environment). + and_return(:fake_terminals) + + is_expected.to eq(:fake_terminals) + end + end + + context 'when the environment does not have terminals' do + before { allow(environment).to receive(:has_terminals?).and_return(false) } + it { is_expected.to eq(nil) } + end + end + + describe '#slug' do + it "is automatically generated" do + expect(environment.slug).not_to be_nil + end + + it "is not regenerated if name changes" do + original_slug = environment.slug + environment.update_attributes!(name: environment.name.reverse) + + expect(environment.slug).to eq(original_slug) + end + end + + describe '#generate_slug' do + SUFFIX = "-[a-z0-9]{6}" + { + "staging-12345678901234567" => "staging-123456789" + SUFFIX, + "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX, + "staging-1234567890123456" => "staging-1234567890123456", + "production" => "production", + "PRODUCTION" => "production" + SUFFIX, + "review/1-foo" => "review-1-foo" + SUFFIX, + "1-foo" => "env-1-foo" + SUFFIX, + "1/foo" => "env-1-foo" + SUFFIX, + "foo-" => "foo" + SUFFIX, + }.each do |name, matcher| + it "returns a slug matching #{matcher}, given #{name}" do + slug = described_class.new(name: name).generate_slug + + expect(slug).to match(/\A#{matcher}\z/) + end + end + end end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 615cfe3142b1b78dc8adc9c178d635a03cc61b09..6004bfdb7b747960da08681e0705fce223bb2282 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe GenericCommitStatus, models: true do - let(:pipeline) { FactoryGirl.create :ci_pipeline } - let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } + let(:pipeline) { create(:ci_pipeline) } + + let(:generic_commit_status) do + create(:generic_commit_status, pipeline: pipeline) + end describe '#context' do subject { generic_commit_status.context } @@ -17,6 +20,15 @@ describe GenericCommitStatus, models: true do it { is_expected.to eq([:external]) } end + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns detailed status object' do + expect(generic_commit_status.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Success + end + end + describe 'set_default_values' do before do generic_commit_status.context = nil diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index dd03348052796f33d31e517349713f2dcc7abacf..d87684fd49eb5c75d3ac968ded162e5db2f64e0b 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -7,26 +7,72 @@ describe GlobalMilestone, models: true do let(:project1) { create(:project, group: group) } let(:project2) { create(:project, path: 'gitlab-ci', group: group) } let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) } - let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } - let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } - let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) } - let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) } - let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) } - let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) } describe '.build_collection' do + let(:milestone1_due_date) { 2.weeks.from_now.to_date } + + let!(:milestone1_project1) do + create( + :milestone, + title: "Milestone v1.2", + project: project1, + due_date: milestone1_due_date + ) + end + + let!(:milestone1_project2) do + create( + :milestone, + title: "Milestone v1.2", + project: project2, + due_date: milestone1_due_date + ) + end + + let!(:milestone1_project3) do + create( + :milestone, + title: "Milestone v1.2", + project: project3, + due_date: milestone1_due_date + ) + end + + let!(:milestone2_project1) do + create( + :milestone, + title: "VD-123", + project: project1, + due_date: nil + ) + end + + let!(:milestone2_project2) do + create( + :milestone, + title: "VD-123", + project: project2, + due_date: nil + ) + end + + let!(:milestone2_project3) do + create( + :milestone, + title: "VD-123", + project: project3, + due_date: nil + ) + end + before do - milestones = - [ - milestone1_project1, - milestone1_project2, - milestone1_project3, - milestone2_project1, - milestone2_project2, - milestone2_project3 - ] + projects = [ + project1, + project2, + project3 + ] - @global_milestones = GlobalMilestone.build_collection(milestones) + @global_milestones = GlobalMilestone.build_collection(projects, {}) end it 'has all project milestones' do @@ -40,9 +86,17 @@ describe GlobalMilestone, models: true do it 'has all project milestones' do expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6) end + + it 'sorts collection by due date' do + expect(@global_milestones.map(&:due_date)).to eq [nil, milestone1_due_date] + end end describe '#initialize' do + let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } + let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } + let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) } + before do milestones = [ diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb index 668aa6fb3572d763152cc480a5ff0d704b068a93..555a876daeb6743dce2d786fb1793c679c9461be 100644 --- a/spec/models/group_label_spec.rb +++ b/spec/models/group_label_spec.rb @@ -43,7 +43,7 @@ describe GroupLabel, models: true do let(:target_project) { build_stubbed(:empty_project, name: 'project-2', namespace: namespace) } it 'returns a String reference to the object' do - expect(label.to_reference(source_project, target_project)).to eq %(project-1~#{label.id}) + expect(label.to_reference(source_project, target_project: target_project)).to eq %(project-1~#{label.id}) end end diff --git a/spec/models/group_milestone_spec.rb b/spec/models/group_milestone_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..601167c3bd325335ffa709de16a94166ac05f7ad --- /dev/null +++ b/spec/models/group_milestone_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe GroupMilestone, models: true do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:project_milestone) do + create(:milestone, title: "Milestone v1.2", project: project) + end + + describe '.build' do + it 'returns milestone with group assigned' do + milestone = GroupMilestone.build( + group, + [project], + project_milestone.title + ) + + expect(milestone.group).to eq group + end + end + + describe '.build_collection' do + before do + project_milestone + end + + it 'returns array of milestones, each with group assigned' do + milestones = GroupMilestone.build_collection(group, [project], {}) + expect(milestones).to all(have_attributes(group: group)) + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 850b1a3cf1ecf0937fd4dee8529bf10ce46e67ff..45fe927202b993bb6725825d750e406346ed5443 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -50,9 +50,8 @@ describe Group, models: true do describe 'validations' do it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_presence_of :path } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.not_to validate_presence_of :owner } end @@ -273,9 +272,20 @@ describe Group, models: true do end describe 'nested group' do - subject { create(:group, :nested) } + subject { build(:group, :nested) } it { is_expected.to be_valid } it { expect(subject.parent).to be_kind_of(Group) } end + + describe '#members_with_parents' do + let!(:group) { create(:group, :nested) } + let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) } + let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) } + + it 'returns parents members' do + expect(group.members_with_parents).to include(developer) + expect(group.members_with_parents).to include(master) + end + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 24e216329a9da98f11454aa29fbd9c0b71742d19..bfa36d92ac3812f37f82f9a1c97e9837c5ad1f62 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,26 +22,6 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end - describe '.visible_to_user' do - let(:user) { create(:user) } - let(:authorized_user) { create(:user) } - let(:project) { create(:project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(Issue.visible_to_user(nil).count).to be(1) - end - - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(Issue.visible_to_user(user).count).to be(1) - end - - it 'returns all issues for user authorized for the issues projects' do - expect(Issue.visible_to_user(authorized_user).count).to be(2) - end - end - describe '#to_reference' do let(:project) { build(:empty_project, name: 'sample-project') } let(:issue) { build(:issue, iid: 1, project: project) } @@ -50,6 +30,10 @@ describe Issue, models: true do expect(issue.to_reference).to eq "#1" end + it 'returns a String reference with the full path' do + expect(issue.to_reference(full: true)).to eq(project.path_with_namespace + '#1') + end + it 'supports a cross-project reference' do another_project = build(:project, name: 'another-project', namespace: project.namespace) expect(issue.to_reference(another_project)).to eq "sample-project#1" diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 2a33d819138c1d71b091dc79c50294305ab53de4..5eaddd822befbdbc9c2e609de802b69d4d114063 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Key, models: true do + include EmailHelpers + describe "Associations" do it { is_expected.to belong_to(:user) } end @@ -26,6 +28,15 @@ describe Key, models: true do expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})") end end + + describe "#update_last_used_at" do + it "enqueues a UseKeyWorker job" do + key = create(:key) + + expect(UseKeyWorker).to receive(:perform_async).with(key.id) + key.update_last_used_at + end + end end context "validation of uniqueness (based on fingerprint uniqueness)" do @@ -96,4 +107,16 @@ describe Key, models: true do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end end + + describe 'notification' do + let(:user) { create(:user) } + + it 'sends a notification' do + perform_enqueued_jobs do + create(:key, user: user) + end + + should_email(user) + end + end end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 0c163659a717ab11279244194eea4a5bb2a4f003..a9139f7d4ab22b4901bb270a9d7d185cae2d9fad 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -31,12 +31,14 @@ describe Label, models: true do it 'validates title' do is_expected.not_to allow_value('G,ITLAB').for(:title) is_expected.not_to allow_value('').for(:title) + is_expected.not_to allow_value('s' * 256).for(:title) is_expected.to allow_value('GITLAB').for(:title) is_expected.to allow_value('gitlab').for(:title) is_expected.to allow_value('G?ITLAB').for(:title) is_expected.to allow_value('G&ITLAB').for(:title) is_expected.to allow_value("customer's request").for(:title) + is_expected.to allow_value('s' * 255).for(:title) end end diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bc278e350f6abe9953d16dbb4d8d1adf032d4dc --- /dev/null +++ b/spec/models/lfs_objects_project_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe LfsObjectsProject, models: true do + subject { create(:lfs_objects_project, project: project) } + let(:project) { create(:empty_project) } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:lfs_object) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:lfs_object_id) } + it { is_expected.to validate_uniqueness_of(:lfs_object_id).scoped_to(:project_id).with_message("already exists in project") } + + it { is_expected.to validate_presence_of(:project_id) } + end + + describe '#update_project_statistics' do + it 'updates project statistics when the object is added' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:lfs_objects_size]) + + subject.save! + end + + it 'updates project statistics when the object is removed' do + subject.save! + + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:lfs_objects_size]) + + subject.destroy + end + end +end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index eb876d105da77b1e97b4e6ca72fb54e5a51be3be..6d599e148a258eb8dd415bb58ae18fa75474cb8f 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -76,6 +76,32 @@ describe MergeRequestDiff, models: true do end end + describe '#save_diffs' do + it 'saves collected state' do + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.collected?).to be_truthy + end + + it 'saves overflow state' do + allow(Commit).to receive(:max_diff_options) + .and_return(max_lines: 0, max_files: 0) + + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.overflow?).to be_truthy + end + + it 'saves empty state' do + allow_any_instance_of(MergeRequestDiff).to receive(:commits) + .and_return([]) + + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.empty?).to be_truthy + end + end + describe '#commits_sha' do it 'returns all commits SHA using serialized commits' do subject.st_commits = [ diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8b730be91fdfa5e249a299fe4198c8dc40c5c044..861426acbc3dcb83719a06d2cb67e4254e64bf13 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -153,6 +153,10 @@ describe MergeRequest, models: true do another_project = build(:project, name: 'another-project', namespace: project.namespace) expect(merge_request.to_reference(another_project)).to eq "sample-project!1" end + + it 'returns a String reference with the full path' do + expect(merge_request.to_reference(full: true)).to eq(project.path_with_namespace + '!1') + end end describe '#raw_diffs' do @@ -205,7 +209,7 @@ describe MergeRequest, models: true do end end - describe "#mr_and_commit_notes" do + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } before do @@ -217,7 +221,7 @@ describe MergeRequest, models: true do it "includes notes for commits" do expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(2) + expect(merge_request.related_notes.count).to eq(2) end it "includes notes for commits from target project as well" do @@ -225,7 +229,7 @@ describe MergeRequest, models: true do project: merge_request.target_project) expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(3) + expect(merge_request.related_notes.count).to eq(3) end end @@ -252,7 +256,7 @@ describe MergeRequest, models: true do end end - describe 'detection of issues to be closed' do + describe '#closes_issues' do let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } @@ -280,14 +284,23 @@ describe MergeRequest, models: true do expect(subject.closes_issues).to be_empty end + end - it 'detects issues mentioned in the description' do - issue2 = create(:issue, project: subject.project) - subject.description = "Closes #{issue2.to_reference}" + describe '#issues_mentioned_but_not_closing' do + let(:closing_issue) { create :issue, project: subject.project } + let(:mentioned_issue) { create :issue, project: subject.project } + + let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") } + + it 'detects issues mentioned in description but not closed' do + subject.project.team << [subject.author, :developer] + subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}" + + allow(subject).to receive(:commits).and_return([commit]) allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.closes_issues).to include(issue2) + expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue]) end end @@ -410,11 +423,17 @@ describe MergeRequest, models: true do .to match("Remove all technical debt\n\n") end - it 'includes its description in the body' do - request = build(:merge_request, description: 'By removing all code') + it 'includes its closed issues in the body' do + issue = create(:issue, project: subject.project) - expect(request.merge_commit_message) - .to match("By removing all code\n\n") + subject.project.team << [subject.author, :developer] + subject.description = "This issue Closes #{issue.to_reference}" + + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) + + expect(subject.merge_commit_message) + .to match("Closes #{issue.to_reference}") end it 'includes its reference in the body' do @@ -429,6 +448,20 @@ describe MergeRequest, models: true do expect(request.merge_commit_message).not_to match("Title\n\n\n\n") end + + it 'includes its description in the body' do + request = build(:merge_request, description: 'By removing all code') + + expect(request.merge_commit_message(include_description: true)) + .to match("By removing all code\n\n") + end + + it 'does not includes its description in the body' do + request = build(:merge_request, description: 'By removing all code') + + expect(request.merge_commit_message) + .not_to match("By removing all code\n\n") + end end describe "#reset_merge_when_build_succeeds" do @@ -1481,6 +1514,108 @@ describe MergeRequest, models: true do end end + describe '#mergeable_with_slash_command?' do + def create_pipeline(status) + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + status: status) + end + + let(:project) { create(:project, :public, only_allow_merge_if_build_succeeds: true) } + let(:developer) { create(:user) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:mr_sha) { merge_request.diff_head_sha } + + before do + project.team << [developer, :developer] + end + + context 'when autocomplete_precheck is set to true' do + it 'is mergeable by developer' do + expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy + end + + it 'is not mergeable by normal user' do + expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey + end + end + + context 'when autocomplete_precheck is set to false' do + it 'is mergeable by developer' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + + it 'is not mergeable by normal user' do + expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey + end + + context 'closed MR' do + before do + merge_request.update_attribute(:state, :closed) + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'MR with WIP' do + before do + merge_request.update_attribute(:title, 'WIP: some MR') + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'sha differs from the MR diff_head_sha' do + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey + end + end + + context 'sha is not provided' do + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey + end + end + + context 'with pipeline ok' do + before do + create_pipeline(:success) + end + + it 'is mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + end + + context 'with failing pipeline' do + before do + create_pipeline(:failed) + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'with running pipeline' do + before do + create_pipeline(:running) + end + + it 'is mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + end + end + end + describe '#has_commits?' do before do allow(subject.merge_request_diff).to receive(:commits_count). diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 0cc2efae5f9b86617716fa685746e3a0470b8e63..064f29d2d66a8a6dac340e23a4a1215c0b1b5fa2 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -24,8 +24,9 @@ describe Milestone, models: true do it { is_expected.to have_many(:issues) } end - let(:milestone) { create(:milestone) } - let(:issue) { create(:issue) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } describe "#title" do @@ -110,8 +111,8 @@ describe Milestone, models: true do describe :items_count do before do - milestone.issues << create(:issue) - milestone.issues << create(:closed_issue) + milestone.issues << create(:issue, project: project) + milestone.issues << create(:closed_issue, project: project) milestone.merge_requests << create(:merge_request) end @@ -126,7 +127,7 @@ describe Milestone, models: true do describe '#total_items_count' do before do - create :closed_issue, milestone: milestone + create :closed_issue, milestone: milestone, project: project create :merge_request, milestone: milestone end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 7f82e85563bcfe02fb2ce342ac3204aefa9b1cd6..600538ff5f435f01e6b2d9b6bbe858efeaa200eb 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -4,22 +4,19 @@ describe Namespace, models: true do let!(:namespace) { create(:namespace) } it { is_expected.to have_many :projects } + it { is_expected.to have_many :project_statistics } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_presence_of(:owner) } - describe "Mass assignment" do - end - describe "Respond to" do it { is_expected.to respond_to(:human_name) } it { is_expected.to respond_to(:to_param) } @@ -61,6 +58,50 @@ describe Namespace, models: true do end end + describe '.with_statistics' do + let(:namespace) { create :namespace } + + let(:project1) do + create(:empty_project, + namespace: namespace, + statistics: build(:project_statistics, + storage_size: 606, + repository_size: 101, + lfs_objects_size: 202, + build_artifacts_size: 303)) + end + + let(:project2) do + create(:empty_project, + namespace: namespace, + statistics: build(:project_statistics, + storage_size: 60, + repository_size: 10, + lfs_objects_size: 20, + build_artifacts_size: 30)) + end + + it "sums all project storage counters in the namespace" do + project1 + project2 + statistics = Namespace.with_statistics.find(namespace.id) + + expect(statistics.storage_size).to eq 666 + expect(statistics.repository_size).to eq 111 + expect(statistics.lfs_objects_size).to eq 222 + expect(statistics.build_artifacts_size).to eq 333 + end + + it "correctly handles namespaces without projects" do + statistics = Namespace.with_statistics.find(namespace.id) + + expect(statistics.storage_size).to eq 0 + expect(statistics.repository_size).to eq 0 + expect(statistics.lfs_objects_size).to eq 0 + expect(statistics.build_artifacts_size).to eq 0 + end + end + describe '#move_dir' do before do @namespace = create :namespace @@ -132,4 +173,26 @@ describe Namespace, models: true do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } end + + describe '#full_name' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_name).to eq(group.name) } + it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } + end + + describe '#parents' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct parents' do + expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.parents).to eq([group, nested_group]) + expect(nested_group.parents).to eq([group]) + expect(group.parents).to eq([]) + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 17a15b12dcb09421deb0684d2bf80a93a499ae46..310fecd8a5cc5f05a190730814ca89ce1e6f9b4c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -162,44 +162,6 @@ describe Note, models: true do end end - describe '.search' do - let(:note) { create(:note_on_issue, note: 'WoW') } - - it 'returns notes with matching content' do - expect(described_class.search(note.note)).to eq([note]) - end - - it 'returns notes with matching content regardless of the casing' do - expect(described_class.search('WOW')).to eq([note]) - end - - context "confidential issues" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } - - it "returns notes with matching content if user can see the issue" do - expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) - end - - it "does not return notes with matching content if user can not see the issue" do - user = create(:user) - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for project members with guest role" do - user = create(:user) - project.team << [user, :guest] - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for unauthenticated users" do - expect(described_class.search(confidential_note.note)).to be_empty - end - end - end - describe "editable?" do it "returns true" do note = build(:note) diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..33ef67f97a7c11b28f8dbb5beb5278c434aa6aaa --- /dev/null +++ b/spec/models/project_authorization_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ProjectAuthorization do + let(:user) { create(:user) } + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } + + describe '.insert_authorizations' do + it 'inserts the authorizations' do + described_class. + insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]]) + + expect(user.project_authorizations.count).to eq(1) + end + + it 'inserts rows in batches' do + described_class.insert_authorizations([ + [user.id, project1.id, Gitlab::Access::MASTER], + [user.id, project2.id, Gitlab::Access::MASTER], + ], 1) + + expect(user.project_authorizations.count).to eq(2) + end + end +end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index d7e1a4e3b6c51d965c0bd185a94e249d66ecec0a..497a626a418c8e2bc9ba635bca6cecc705360844 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -1,14 +1,28 @@ require 'spec_helper' -describe BambooService, models: true do +describe BambooService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:bamboo_url) { 'http://gitlab.com/bamboo' } + + subject(:service) do + described_class.create( + project: create(:empty_project), + properties: { + bamboo_url: bamboo_url, + username: 'mic', + password: 'password', + build_key: 'foo' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end describe 'Validations' do - subject { service } - context 'when service is active' do before { subject.active = true } @@ -103,90 +117,103 @@ describe BambooService, models: true do end describe '#build_page' do - it 'returns a specific URL when status is 500' do - stub_request(status: 500) + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo') + expect(service.build_page('sha', 'ref')).to eq('foo') end + end - it 'returns a specific URL when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo') + expect(service.commit_status('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when bamboo_url has no trailing slash' do - stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + describe '#calculate_reactive_cache' do + context '#build_page' do + subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } - expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42') - end + it 'returns a specific URL when status is 500' do + stub_request(status: 500) - it 'returns a build URL when bamboo_url has a trailing slash' do - stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + is_expected.to eq('http://gitlab.com/bamboo/browse/foo') + end - expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42') - end - end + it 'returns a specific URL when response has no results' do + stub_request(body: bamboo_response(size: 0)) - describe '#commit_status' do - it 'sets commit status to :error when status is 500' do - stub_request(status: 500) + is_expected.to eq('http://gitlab.com/bamboo/browse/foo') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end + it 'returns a build URL when bamboo_url has no trailing slash' do + stub_request(body: bamboo_response) - it 'sets commit status to "pending" when status is 404' do - stub_request(status: 404) + is_expected.to eq('http://gitlab.com/bamboo/browse/42') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + context 'bamboo_url has trailing slash' do + let(:bamboo_url) { 'http://gitlab.com/bamboo/' } - it 'sets commit status to "pending" when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + it 'returns a build URL' do + stub_request(body: bamboo_response) - expect(service.commit_status('123', 'unused')).to eq('pending') + is_expected.to eq('http://gitlab.com/bamboo/browse/42') + end + end end - it 'sets commit status to "success" when build state contains Success' do - stub_request(build_state: 'YAY Success!') + context '#commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } - expect(service.commit_status('123', 'unused')).to eq('success') - end + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) - it 'sets commit status to "failed" when build state contains Failed' do - stub_request(build_state: 'NO Failed!') + is_expected.to eq(:error) + end - expect(service.commit_status('123', 'unused')).to eq('failed') - end + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) - it 'sets commit status to "pending" when build state contains Pending' do - stub_request(build_state: 'NO Pending!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + it 'sets commit status to "pending" when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) - it 'sets commit status to :error when build state is unknown' do - stub_request(build_state: 'FOO BAR!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end - end + it 'sets commit status to "success" when build state contains Success' do + stub_request(body: bamboo_response(build_state: 'YAY Success!')) - def service(bamboo_url: 'http://gitlab.com/bamboo') - described_class.create( - project: create(:empty_project), - properties: { - bamboo_url: bamboo_url, - username: 'mic', - password: 'password', - build_key: 'foo' - } - ) + is_expected.to eq('success') + end + + it 'sets commit status to "failed" when build state contains Failed' do + stub_request(body: bamboo_response(build_state: 'NO Failed!')) + + is_expected.to eq('failed') + end + + it 'sets commit status to "pending" when build state contains Pending' do + stub_request(body: bamboo_response(build_state: 'NO Pending!')) + + is_expected.to eq('pending') + end + + it 'sets commit status to :error when build state is unknown' do + stub_request(body: bamboo_response(build_state: 'FOO BAR!')) + + is_expected.to eq(:error) + end + end end - def stub_request(status: 200, body: nil, build_state: 'success') + def stub_request(status: 200, body: nil) bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic' - body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}}) WebMock.stub_request(:get, bamboo_full_url).to_return( status: status, @@ -194,4 +221,8 @@ describe BambooService, models: true do body: body ) end + + def bamboo_response(result_key: 42, build_state: 'success', size: 1) + %Q({"results":{"results":{"size":"#{size}","result":{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}}}}) + end end diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 6f65beb79d0e8bb86cbe93d3a402aa20b1d4449e..dbd23ff54918dd654f47a3601b594cb627193fff 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -1,6 +1,21 @@ require 'spec_helper' -describe BuildkiteService, models: true do +describe BuildkiteService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:project) { create(:empty_project) } + + subject(:service) do + described_class.create( + project: project, + properties: { + service_hook: true, + project_url: 'https://buildkite.com/account-name/example-project', + token: 'secret-sauce-webhook-token:secret-sauce-status-token' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -25,21 +40,12 @@ describe BuildkiteService, models: true do describe 'commits methods' do before do - @project = Project.new - allow(@project).to receive(:default_branch).and_return('default-brancho') - - @service = BuildkiteService.new - allow(@service).to receive_messages( - project: @project, - service_hook: true, - project_url: 'https://buildkite.com/account-name/example-project', - token: 'secret-sauce-webhook-token:secret-sauce-status-token' - ) + allow(project).to receive(:default_branch).and_return('default-brancho') end describe '#webhook_url' do it 'returns the webhook url' do - expect(@service.webhook_url).to eq( + expect(service.webhook_url).to eq( 'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token' ) end @@ -47,7 +53,7 @@ describe BuildkiteService, models: true do describe '#commit_status_path' do it 'returns the correct status page' do - expect(@service.commit_status_path('2ab7834c')).to eq( + expect(service.commit_status_path('2ab7834c')).to eq( 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c' ) end @@ -55,10 +61,53 @@ describe BuildkiteService, models: true do describe '#build_page' do it 'returns the correct build page' do - expect(@service.build_page('2ab7834c', nil)).to eq( + expect(service.build_page('2ab7834c', nil)).to eq( 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c' ) end end + + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') + + expect(service.commit_status('sha', 'ref')).to eq('foo') + end + end + + describe '#calculate_reactive_cache' do + context '#commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } + + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + is_expected.to eq(:error) + end + + it 'sets commit status to :error when status is 404' do + stub_request(status: 404) + + is_expected.to eq(:error) + end + + it 'passes through build status untouched when status is 200' do + stub_request(body: %Q({"status":"Great Success"})) + + is_expected.to eq('Great Success') + end + end + end + end + + def stub_request(status: 200, body: nil) + body ||= %Q({"status":"success"}) + buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123' + + WebMock.stub_request(:get, buildkite_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) end end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb similarity index 81% rename from spec/models/project_services/slack_service/build_message_spec.rb rename to spec/models/project_services/chat_message/build_message_spec.rb index 452f4e2782c80568f71bffc76f8deeaa6438b0ed..50ad5013df94e8c541cf4ce594d6436fa6d8a11c 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::BuildMessage do - subject { SlackService::BuildMessage.new(args) } +describe ChatMessage::BuildMessage do + subject { described_class.new(args) } let(:args) do { @@ -10,7 +10,7 @@ describe SlackService::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'example.gitlab.com', + project_url: 'http://example.gitlab.com', commit: { status: status, @@ -48,10 +48,10 @@ describe SlackService::BuildMessage do end def build_message(status_text = status) - "<example.gitlab.com|project_name>:" \ - " Commit <example.gitlab.com/commit/" \ + "<http://example.gitlab.com|project_name>:" \ + " Commit <http://example.gitlab.com/commit/" \ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ - " of <example.gitlab.com/commits/develop|develop> branch" \ + " of <http://example.gitlab.com/commits/develop|develop> branch" \ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb similarity index 75% rename from spec/models/project_services/slack_service/issue_message_spec.rb rename to spec/models/project_services/chat_message/issue_message_spec.rb index 98c36ec088dde79355b9a16943f194e68abbc285..190ff4c535df6e66f23b4e8d58199d439a48dd8d 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::IssueMessage, models: true do - subject { SlackService::IssueMessage.new(args) } +describe ChatMessage::IssueMessage, models: true do + subject { described_class.new(args) } let(:args) do { @@ -10,14 +10,14 @@ describe SlackService::IssueMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: 'Issue title', id: 10, iid: 100, assignee_id: 1, - url: 'url', + url: 'http://url.com', action: 'open', state: 'opened', description: 'issue description' @@ -40,11 +40,11 @@ describe SlackService::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - '<somewhere.com|[project_name>] Issue opened by test.user') + '[<http://somewhere.com|project_name>] Issue opened by test.user') expect(subject.attachments).to eq([ { title: "#100 Issue title", - title_link: "url", + title_link: "http://url.com", text: "issue description", color: color, } @@ -60,7 +60,7 @@ describe SlackService::IssueMessage, models: true do it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user') + '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by test.user') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb similarity index 66% rename from spec/models/project_services/slack_service/merge_message_spec.rb rename to spec/models/project_services/chat_message/merge_message_spec.rb index c5c052d9af1b0e342bf0493f096f9ab7be7ad404..cc154112e90d9c18e72f2518a5936f51188f69e1 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::MergeMessage, models: true do - subject { SlackService::MergeMessage.new(args) } +describe ChatMessage::MergeMessage, models: true do + subject { described_class.new(args) } let(:args) do { @@ -10,14 +10,14 @@ describe SlackService::MergeMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: "Issue title\nSecond line", id: 10, iid: 100, assignee_id: 1, - url: 'url', + url: 'http://url.com', state: 'opened', description: 'issue description', source_branch: 'source_branch', @@ -31,8 +31,8 @@ describe SlackService::MergeMessage, models: true do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\ - 'in <somewhere.com|project_name>: *Issue title*') + 'test.user opened <http://somewhere.com/merge_requests/100|merge request !100> '\ + 'in <http://somewhere.com|project_name>: *Issue title*') expect(subject.attachments).to be_empty end end @@ -43,8 +43,8 @@ describe SlackService::MergeMessage, models: true do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\ - 'in <somewhere.com|project_name>: *Issue title*') + 'test.user closed <http://somewhere.com/merge_requests/100|merge request !100> '\ + 'in <http://somewhere.com|project_name>: *Issue title*') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb similarity index 76% rename from spec/models/project_services/slack_service/note_message_spec.rb rename to spec/models/project_services/chat_message/note_message_spec.rb index 97f818125d33155cf1aea8f27098a97fa636e9a0..da700a08e574806c1be85ad48d04537be857c392 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::NoteMessage, models: true do +describe ChatMessage::NoteMessage, models: true do let(:color) { '#345' } before do @@ -11,15 +11,15 @@ describe SlackService::NoteMessage, models: true do avatar_url: 'http://fakeavatar' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', repository: { name: 'project_name', - url: 'somewhere.com', + url: 'http://somewhere.com', }, object_attributes: { id: 10, note: 'comment on a commit', - url: 'url', + url: 'http://url.com', noteable_type: 'Commit' } } @@ -36,9 +36,9 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on commits' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user <url|commented on " \ - "commit 5f163b2b> in <somewhere.com|project_name>: " \ + message = described_class.new(@args) + expect(message.pretext).to eq("test.user <http://url.com|commented on " \ + "commit 5f163b2b> in <http://somewhere.com|project_name>: " \ "*Added a commit message*") expected_attachments = [ { @@ -62,9 +62,9 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on a merge request' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user <url|commented on " \ - "merge request !30> in <somewhere.com|project_name>: " \ + message = described_class.new(@args) + expect(message.pretext).to eq("test.user <http://url.com|commented on " \ + "merge request !30> in <http://somewhere.com|project_name>: " \ "*merge request title*") expected_attachments = [ { @@ -88,10 +88,10 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on an issue' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq( - "test.user <url|commented on " \ - "issue #20> in <somewhere.com|project_name>: " \ + "test.user <http://url.com|commented on " \ + "issue #20> in <http://somewhere.com|project_name>: " \ "*issue title*") expected_attachments = [ { @@ -114,9 +114,9 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on a project snippet' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user <url|commented on " \ - "snippet #5> in <somewhere.com|project_name>: " \ + message = described_class.new(@args) + expect(message.pretext).to eq("test.user <http://url.com|commented on " \ + "snippet #5> in <http://somewhere.com|project_name>: " \ "*snippet title*") expected_attachments = [ { diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bf2a96164551f31e5a46440f7a439514284eddf5 --- /dev/null +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe ChatMessage::PipelineMessage do + subject { described_class.new(args) } + let(:user) { { name: 'hacker' } } + + let(:args) do + { + object_attributes: { + id: 123, + sha: '97de212e80737a608d939f648d959671fb0a0142', + tag: false, + ref: 'develop', + status: status, + duration: duration + }, + project: { path_with_namespace: 'project_name', + web_url: 'http://example.gitlab.com' }, + user: user + } + end + + let(:message) { build_message } + + context 'pipeline succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + verify_message + end + end + + context 'pipeline failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + verify_message + end + + context 'when triggered by API therefore lacking user' do + let(:user) { nil } + let(:message) { build_message(status, 'API') } + + it 'returns a message stating it is by API' do + verify_message + end + end + end + + def verify_message + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + + def build_message(status_text = status, name = user[:name]) + "<http://example.gitlab.com|project_name>:" \ + " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \ + " of <http://example.gitlab.com/commits/develop|develop> branch" \ + " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb similarity index 57% rename from spec/models/project_services/slack_service/push_message_spec.rb rename to spec/models/project_services/chat_message/push_message_spec.rb index 17cd05e24f1291e4765fc70ae839f11e0da82c6f..24928873bada700febf67896e84f41f830460af0 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::PushMessage, models: true do - subject { SlackService::PushMessage.new(args) } +describe ChatMessage::PushMessage, models: true do + subject { described_class.new(args) } let(:args) do { @@ -10,7 +10,7 @@ describe SlackService::PushMessage, models: true do project_name: 'project_name', ref: 'refs/heads/master', user_name: 'test.user', - project_url: 'url' + project_url: 'http://url.com' } end @@ -19,20 +19,20 @@ describe SlackService::PushMessage, models: true do context 'push' do before do args[:commits] = [ - { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, - { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, + { message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }, ] end it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch <url/commits/master|master> of '\ - '<url|project_name> (<url/compare/before...after|Compare changes>)' + 'test.user pushed to branch <http://url.com/commits/master|master> of '\ + '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)' ) expect(subject.attachments).to eq([ { - text: "<url1|abcdefgh>: message1 - author1\n"\ - "<url2|12345678>: message2 - author2", + text: "<http://url1.com|abcdefgh>: message1 - author1\n"\ + "<http://url2.com|12345678>: message2 - author2", color: color, } ]) @@ -47,14 +47,14 @@ describe SlackService::PushMessage, models: true do project_name: 'project_name', ref: 'refs/tags/new_tag', user_name: 'test.user', - project_url: 'url' + project_url: 'http://url.com' } end it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - '<url/commits/new_tag|new_tag> to ' \ - '<url|project_name>') + '<http://url.com/commits/new_tag|new_tag> to ' \ + '<http://url.com|project_name>') expect(subject.attachments).to be_empty end end @@ -66,8 +66,8 @@ describe SlackService::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch <url/commits/master|master> to '\ - '<url|project_name>' + 'test.user pushed new branch <http://url.com/commits/master|master> to '\ + '<http://url.com|project_name>' ) expect(subject.attachments).to be_empty end @@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch master from <url|project_name>' + 'test.user removed branch master from <http://url.com|project_name>' ) expect(subject.attachments).to be_empty end diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb similarity index 83% rename from spec/models/project_services/slack_service/wiki_page_message_spec.rb rename to spec/models/project_services/chat_message/wiki_page_message_spec.rb index 093911598b009e29a2a606c2231f0968e5cd82e1..a2ad61e38e76355eb2cf18d2df204ac9f3f92992 100644 --- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::WikiPageMessage, models: true do +describe ChatMessage::WikiPageMessage, models: true do subject { described_class.new(args) } let(:args) do @@ -10,10 +10,10 @@ describe SlackService::WikiPageMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: 'Wiki page title', - url: 'url', + url: 'http://url.com', content: 'Wiki page description' } } @@ -25,7 +25,7 @@ describe SlackService::WikiPageMessage, models: true do it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( - 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\ + 'test.user created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\ '*Wiki page title*') end end @@ -35,7 +35,7 @@ describe SlackService::WikiPageMessage, models: true do it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( - 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\ + 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\ '*Wiki page title*') end end diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c98e7ee14fdf76cd2bd7b694f52a07164ffc4bf3 --- /dev/null +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe ChatNotificationService, models: true do + describe "Associations" do + before do + allow(subject).to receive(:activated?).and_return(true) + end + + it { is_expected.to validate_presence_of :webhook } + end +end diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb deleted file mode 100644 index c6a45a3e1be1f5c6354e8ec2552c8cc1f0e69089..0000000000000000000000000000000000000000 --- a/spec/models/project_services/chat_service_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe ChatService, models: true do - describe "Associations" do - it { is_expected.to have_many :chat_names } - end - - describe '#valid_token?' do - subject { described_class.new } - - it 'is false as it has no token' do - expect(subject.valid_token?('wer')).to be_falsey - end - end -end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index f13bb1e8adfe3d89fc88cd87a95ac609a1c36539..42c2ed668bcf78efb08f859c3d3d5dc4c2d6a28b 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' -describe DroneCiService, models: true do +describe DroneCiService, models: true, caching: true do + include ReactiveCachingHelpers + describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to have_one(:service_hook) } @@ -33,6 +35,10 @@ describe DroneCiService, models: true do let(:token) { 'secret' } let(:iid) { rand(1..9999) } + # URL's + let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } + let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } + before(:each) do allow(drone).to receive_messages( project_id: project.id, @@ -42,22 +48,66 @@ describe DroneCiService, models: true do token: token ) end + + def stub_request(status: 200, body: nil) + body ||= %Q({"status":"success"}) + + WebMock.stub_request(:get, commit_status_path).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end describe "service page/path methods" do include_context :drone_ci_service - # URL's - let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } - let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" } - let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } - let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" } - - it { expect(drone.build_page(sha, branch)).to eq(commit_page) } - it { expect(drone.commit_page(sha, branch)).to eq(commit_page) } - it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) } + it { expect(drone.build_page(sha, branch)).to eq(build_page) } it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) } - it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) } + end + + describe '#commit_status' do + include_context :drone_ci_service + + it 'returns the contents of the reactive cache' do + stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref') + + expect(drone.commit_status('sha', 'ref')).to eq('foo') + end + end + + describe '#calculate_reactive_cache' do + include_context :drone_ci_service + + context '#commit_status' do + subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] } + + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + is_expected.to eq(:error) + end + + it 'sets commit status to :error when status is 404' do + stub_request(status: 404) + + is_expected.to eq(:error) + end + + { "killed" => :canceled, + "failure" => :failed, + "error" => :failed, + "success" => "success", + }.each do |drone_status, our_status| + + it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do + stub_request(body: %Q({"status":"#{drone_status}"})) + + is_expected.to eq(our_status) + end + end + end end describe "execute" do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f3cd14e941322efb4094e11c09ba3c1e8d8b274 --- /dev/null +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +describe KubernetesService, models: true, caching: true do + include KubernetesHelpers + include ReactiveCachingHelpers + + let(:project) { create(:kubernetes_project) } + let(:service) { project.kubernetes_service } + + # We use Kubeclient to interactive with the Kubernetes API. It will + # GET /api/v1 for a list of resources the API supports. This must be stubbed + # in addition to any other HTTP requests we expect it to perform. + let(:discovery_url) { service.api_url + '/api/v1' } + let(:discovery_response) { { body: kube_discovery_body.to_json } } + + let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" } + let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } } + + def stub_kubeclient_discover + WebMock.stub_request(:get, discovery_url).to_return(discovery_response) + end + + def stub_kubeclient_pods + stub_kubeclient_discover + WebMock.stub_request(:get, pods_url).to_return(pods_response) + end + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:api_url) } + it { is_expected.to validate_presence_of(:token) } + + context 'namespace format' do + before do + subject.project = project + subject.api_url = "http://example.com" + subject.token = "test" + end + + { + 'foo' => true, + '1foo' => true, + 'foo1' => true, + 'foo-bar' => true, + '-foo' => false, + 'foo-' => false, + 'a' * 63 => true, + 'a' * 64 => false, + 'a.b' => false, + 'a*b' => false, + }.each do |namespace, validity| + it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do + subject.namespace = namespace + + expect(subject.valid?).to eq(validity) + end + end + end + end + + context 'when service is inactive' do + before { subject.active = false } + it { is_expected.not_to validate_presence_of(:namespace) } + it { is_expected.not_to validate_presence_of(:api_url) } + it { is_expected.not_to validate_presence_of(:token) } + end + end + + describe '#initialize_properties' do + context 'with a project' do + it 'defaults to the project name' do + expect(described_class.new(project: project).namespace).to eq(project.name) + end + end + + context 'without a project' do + it 'leaves the namespace unset' do + expect(described_class.new.namespace).to be_nil + end + end + end + + describe '#test' do + before do + stub_kubeclient_discover + end + + context 'with path prefix in api_url' do + let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } + + it 'tests with the prefix' do + service.api_url = 'https://kubernetes.example.com/prefix/' + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'with custom CA certificate' do + it 'is added to the certificate store' do + service.ca_pem = "CA PEM DATA" + + cert = double("certificate") + expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert) + expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'success' do + it 'reads the discovery endpoint' do + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'failure' do + let(:discovery_response) { { status: 404 } } + + it 'fails to read the discovery endpoint' do + expect(service.test[:success]).to be_falsy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + end + + describe '#predefined_variables' do + before do + subject.api_url = 'https://kube.domain.com' + subject.token = 'token' + subject.namespace = 'my-project' + subject.ca_pem = 'CA PEM DATA' + end + + it 'sets KUBE_URL' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true } + ) + end + + it 'sets KUBE_TOKEN' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_TOKEN', value: 'token', public: false } + ) + end + + it 'sets KUBE_NAMESPACE' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_NAMESPACE', value: 'my-project', public: true } + ) + end + + it 'sets KUBE_CA_PEM' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true } + ) + end + end + + describe '#terminals' do + let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } + subject { service.terminals(environment) } + + context 'with invalid pods' do + it 'returns no terminals' do + stub_reactive_cache(service, pods: [ { "bad" => "pod" } ]) + + is_expected.to be_empty + end + end + + context 'with valid pods' do + let(:pod) { kube_pod(app: environment.slug) } + let(:terminals) { kube_terminals(service, pod) } + + it 'returns terminals' do + stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]) + + is_expected.to eq(terminals + terminals) + end + end + end + + describe '#calculate_reactive_cache' do + before { stub_kubeclient_pods } + subject { service.calculate_reactive_cache } + + context 'when service is inactive' do + before { service.active = false } + + it { is_expected.to be_nil } + end + + context 'when kubernetes responds with valid pods' do + it { is_expected.to eq(pods: [kube_pod]) } + end + + context 'when kubernetes responds with 500' do + let(:pods_response) { { status: 500 } } + + it { expect { subject }.to raise_error(KubeException) } + end + + context 'when kubernetes responds with 404' do + let(:pods_response) { { status: 404 } } + + it { is_expected.to eq(pods: []) } + end + end +end diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..490d6aedffce7339a17b8adf385719790a28157d --- /dev/null +++ b/spec/models/project_services/mattermost_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostService, models: true do + it_behaves_like "slack or mattermost notifications" +end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 4a1037e950b7b10d21c612dfd15038883919a69e..c879edddfdd4fdbae7ef6ffb8c0dcb2e5bfbfab3 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -1,97 +1,122 @@ require 'spec_helper' -describe MattermostSlashCommandsService, models: true do - describe "Associations" do - it { is_expected.to respond_to :token } - end +describe MattermostSlashCommandsService, :models do + it_behaves_like "chat slash commands service" - describe '#valid_token?' do - subject { described_class.new } + context 'Mattermost API' do + let(:project) { create(:empty_project) } + let(:service) { project.build_mattermost_slash_commands_service } + let(:user) { create(:user) } - context 'when the token is empty' do - it 'is false' do - expect(subject.valid_token?('wer')).to be_falsey - end + before do + Mattermost::Session.base_uri("http://mattermost.example.com") + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) end - context 'when there is a token' do - before do - subject.token = '123' + describe '#configure' do + subject do + service.configure(user, team_id: 'abc', + trigger: 'gitlab', url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png') end - it 'accepts equal tokens' do - expect(subject.valid_token?('123')).to be_truthy - end - end - end + context 'the requests succeeds' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab', + url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png', + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{project.name_with_namespace}", + display_name: "GitLab / #{project.name_with_namespace}", + method: 'P', + username: 'GitLab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end - describe '#trigger' do - subject { described_class.new } + it 'saves the service' do + expect { subject }.to change { project.services.count }.by(1) + end - context 'no token is passed' do - let(:params) { Hash.new } + it 'saves the token' do + subject - it 'returns nil' do - expect(subject.trigger(params)).to be_nil + expect(service.reload.token).to eq('token') + end end - end - context 'with a token passed' do - let(:project) { create(:empty_project) } - let(:params) { { token: 'token' } } - - before do - allow(subject).to receive(:token).and_return('token') - end + context 'an error is received' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end - context 'no user can be found' do - context 'when no url can be generated' do - it 'responds with the authorize url' do - response = subject.trigger(params) + it 'shows error messages' do + succeeded, message = subject - expect(response[:response_type]).to eq :ephemeral - expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" - end + expect(succeeded).to be(false) + expect(message).to eq('This trigger word is already in use. Please choose another word.') end + end + end - context 'when an auth url can be generated' do - let(:params) do - { - team_domain: 'http://domain.tld', - team_id: 'T3423423', - user_id: 'U234234', - user_name: 'mepmep', - token: 'token' - } - end - - let(:service) do - project.create_mattermost_slash_commands_service( - properties: { token: 'token' } - ) - end + describe '#list_teams' do + subject do + service.list_teams(user) + end - it 'generates the url' do - response = service.trigger(params) + context 'the requests succeeds' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: ['list'].to_json + ) + end - expect(response[:text]).to start_with(':wave: Hi there!') - end + it 'returns a list of teams' do + expect(subject).not_to be_empty end end - context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, service: service) } - let(:service) do - project.create_mattermost_slash_commands_service( - properties: { token: 'token' } - ) + context 'an error is received' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + message: 'Failed to get team list.' + }.to_json + ) end - let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } - it 'triggers the command' do - expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + it 'shows error messages' do + teams, message = subject - service.trigger(params) + expect(teams).to be_empty + expect(message).to eq('Failed to get team list.') end end end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb deleted file mode 100644 index 363138a9454ca5cec4e65a036ade72c74d01163c..0000000000000000000000000000000000000000 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe SlackService::PipelineMessage do - subject { SlackService::PipelineMessage.new(args) } - - let(:args) do - { - object_attributes: { - id: 123, - sha: '97de212e80737a608d939f648d959671fb0a0142', - tag: false, - ref: 'develop', - status: status, - duration: duration - }, - project: { path_with_namespace: 'project_name', - web_url: 'example.gitlab.com' }, - user: { name: 'hacker' } - } - end - - let(:message) { build_message } - - context 'pipeline succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:duration) { 10 } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - context 'pipeline failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 10 } - - it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - def build_message(status_text = status) - "<example.gitlab.com|project_name>:" \ - " Pipeline <example.gitlab.com/pipelines/123|#123>" \ - " of <example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index c07a70a806965975c637e5a877b7225811c95a71..9a3ecc66d836683a6a168ef4cfade93b22128ec8 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -1,327 +1,5 @@ require 'spec_helper' describe SlackService, models: true do - let(:slack) { SlackService.new } - let(:webhook_url) { 'https://example.gitlab.com/' } - - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - describe 'Validations' do - context 'when service is active' do - before { subject.active = true } - - it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like 'issue tracker service URL attribute', :webhook - end - - context 'when service is inactive' do - before { subject.active = false } - - it { is_expected.not_to validate_presence_of(:webhook) } - end - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } - - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end - - before do - allow(slack).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - - opts = { - title: 'Awesome issue', - description: 'please fix' - } - - issue_service = Issues::CreateService.new(project, user, opts) - @issue = issue_service.execute - @issues_sample_data = issue_service.hook_data(@issue, 'open') - - opts = { - title: 'Awesome merge_request', - description: 'please fix', - source_branch: 'feature', - target_branch: 'master' - } - merge_service = MergeRequests::CreateService.new(project, - user, opts) - @merge_request = merge_service.execute - @merge_sample_data = merge_service.hook_data(@merge_request, - 'open') - - opts = { - title: "Awesome wiki_page", - content: "Some text describing some thing or another", - format: "md", - message: "user created page: Awesome wiki_page" - } - - wiki_page_service = WikiPages::CreateService.new(project, user, opts) - @wiki_page = wiki_page_service.execute - @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') - end - - it "calls Slack API for push events" do - slack.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for issue events" do - slack.execute(@issues_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for merge requests events" do - slack.execute(@merge_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for wiki page events" do - slack.execute(@wiki_page_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it 'uses the username as an option for slack when configured' do - allow(slack).to receive(:username).and_return(username) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(push_sample_data) - end - - it 'uses the channel as an option when it is configured' do - allow(slack).to receive(:channel).and_return(channel) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: channel). - and_return( - double(:slack_service).as_null_object - ) - slack.execute(push_sample_data) - end - - context "event channels" do - it "uses the right channel for push event" do - slack.update_attributes(push_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(push_sample_data) - end - - it "uses the right channel for merge request event" do - slack.update_attributes(merge_request_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@merge_sample_data) - end - - it "uses the right channel for issue event" do - slack.update_attributes(issue_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@issues_sample_data) - end - - it "uses the right channel for wiki event" do - slack.update_attributes(wiki_page_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@wiki_page_sample_data) - end - - context "note event" do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "uses the right channel" do - slack.update_attributes(note_channel: "random") - - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(note_data) - end - end - end - end - - describe "Note events" do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id) } - - before do - allow(slack).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - end - - context 'when commit comment event executed' do - let(:commit_note) do - create(:note_on_commit, author: user, - project: project, - commit_id: project.repository.commit.id, - note: 'a comment on a commit') - end - - it "calls Slack API for commit comment events" do - data = Gitlab::DataBuilder::Note.build(commit_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when merge request comment event executed' do - let(:merge_request_note) do - create(:note_on_merge_request, project: project, - note: "merge request note") - end - - it "calls Slack API for merge request comment events" do - data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when issue comment event executed' do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "calls Slack API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when snippet comment event executed' do - let(:snippet_note) do - create(:note_on_project_snippet, project: project, - note: "snippet note") - end - - it "calls Slack API for snippet comment events" do - data = Gitlab::DataBuilder::Note.build(snippet_note, user) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - end - - describe 'Pipeline events' do - let(:user) { create(:user) } - let(:project) { create(:project) } - - let(:pipeline) do - create(:ci_pipeline, - project: project, status: status, - sha: project.commit.sha, ref: project.default_branch) - end - - before do - allow(slack).to receive_messages( - project: project, - service_hook: true, - webhook: webhook_url - ) - end - - shared_examples 'call Slack API' do - before do - WebMock.stub_request(:post, webhook_url) - end - - it 'calls Slack API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - slack.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'with failed pipeline' do - let(:status) { 'failed' } - - it_behaves_like 'call Slack API' - end - - context 'with succeeded pipeline' do - let(:status) { 'success' } - - context 'with default to notify_only_broken_pipelines' do - it 'does not call Slack API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - result = slack.execute(data) - - expect(result).to be_falsy - end - end - - context 'with setting notify_only_broken_pipelines to false' do - before do - slack.notify_only_broken_pipelines = false - end - - it_behaves_like 'call Slack API' - end - end - end + it_behaves_like "slack or mattermost notifications" end diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5766aa340e26cfba6fcf67dfcc82a61726b684ee --- /dev/null +++ b/spec/models/project_services/slack_slash_commands_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe SlackSlashCommandsService, :models do + it_behaves_like "chat slash commands service" + + describe '#trigger' do + context 'when an auth url is generated' do + let(:project) { create(:empty_project) } + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + + let(:service) do + project.create_slack_slash_commands_service( + properties: { token: 'token' }, + active: true + ) + end + + let(:authorize_url) do + 'http://authorize.example.com/' + end + + before do + allow(service).to receive(:authorize_chat_name_url).and_return(authorize_url) + end + + it 'uses slack compatible links' do + response = service.trigger(params) + + expect(response[:text]).to include("<#{authorize_url}|connect your GitLab account>") + end + end + end +end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index f7e878844dcff4d87a01b589aa45242abccfb8a3..a1edd083aa1c456bffdeae8855840eef648c02e4 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -1,14 +1,28 @@ require 'spec_helper' -describe TeamcityService, models: true do +describe TeamcityService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:teamcity_url) { 'http://gitlab.com/teamcity' } + + subject(:service) do + described_class.create( + project: create(:empty_project), + properties: { + teamcity_url: teamcity_url, + username: 'mic', + password: 'password', + build_type: 'foo' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end describe 'Validations' do - subject { service } - context 'when service is active' do before { subject.active = true } @@ -103,73 +117,87 @@ describe TeamcityService, models: true do end describe '#build_page' do - it 'returns a specific URL when status is 500' do - stub_request(status: 500) + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + expect(service.build_page('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when teamcity_url has no trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') - expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + expect(service.commit_status('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when teamcity_url has a trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + describe '#calculate_reactive_cache' do + context 'build_page' do + subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } - expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') - end - end + it 'returns a specific URL when status is 500' do + stub_request(status: 500) - describe '#commit_status' do - it 'sets commit status to :error when status is 500' do - stub_request(status: 500) + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end + it 'returns a build URL when teamcity_url has no trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) - it 'sets commit status to "pending" when status is 404' do - stub_request(status: 404) + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + context 'teamcity_url has trailing slash' do + let(:teamcity_url) { 'http://gitlab.com/teamcity/' } - it 'sets commit status to "success" when build status contains SUCCESS' do - stub_request(build_status: 'YAY SUCCESS!') + it 'returns a build URL' do + stub_request(body: %Q({"build":{"id":"666"}})) - expect(service.commit_status('123', 'unused')).to eq('success') + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + end + end end - it 'sets commit status to "failed" when build status contains FAILURE' do - stub_request(build_status: 'NO FAILURE!') + context 'commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } - expect(service.commit_status('123', 'unused')).to eq('failed') - end + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) - it 'sets commit status to "pending" when build status contains Pending' do - stub_request(build_status: 'NO Pending!') + is_expected.to eq(:error) + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) - it 'sets commit status to :error when build status is unknown' do - stub_request(build_status: 'FOO BAR!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end - end + it 'sets commit status to "success" when build status contains SUCCESS' do + stub_request(build_status: 'YAY SUCCESS!') - def service(teamcity_url: 'http://gitlab.com/teamcity') - described_class.create( - project: create(:empty_project), - properties: { - teamcity_url: teamcity_url, - username: 'mic', - password: 'password', - build_type: 'foo' - } - ) + is_expected.to eq('success') + end + + it 'sets commit status to "failed" when build status contains FAILURE' do + stub_request(build_status: 'NO FAILURE!') + + is_expected.to eq('failed') + end + + it 'sets commit status to "pending" when build status contains Pending' do + stub_request(build_status: 'NO Pending!') + + is_expected.to eq('pending') + end + + it 'sets commit status to :error when build status is unknown' do + stub_request(build_status: 'FOO BAR!') + + is_expected.to eq(:error) + end + end end def stub_request(status: 200, body: nil, build_status: 'success') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21ff238841ea237d6a0f9db719e9a3b262b726ea..e93a4e62244f33fbe1716ac00251ee8ee4cef1ad 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -20,9 +20,9 @@ describe Project, models: true do it { is_expected.to have_many(:deploy_keys) } it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } - it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } @@ -36,6 +36,7 @@ describe Project, models: true do it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } it { is_expected.to have_one(:flowdock_service).dependent(:destroy) } it { is_expected.to have_one(:assembla_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_slash_commands_service).dependent(:destroy) } it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) } it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) } it { is_expected.to have_one(:buildkite_service).dependent(:destroy) } @@ -48,6 +49,7 @@ describe Project, models: true do it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) } it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) } it { is_expected.to have_one(:project_feature).dependent(:destroy) } + it { is_expected.to have_one(:statistics).class_name('ProjectStatistics').dependent(:delete) } it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) } it { is_expected.to have_one(:last_event).class_name('Event') } it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) } @@ -188,34 +190,54 @@ describe Project, models: true do end it 'does not allow an invalid URI as import_url' do - project2 = build(:project, import_url: 'invalid://') + project2 = build(:empty_project, import_url: 'invalid://') expect(project2).not_to be_valid end it 'does allow a valid URI as import_url' do - project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git') + project2 = build(:empty_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: '') + project2 = build(:empty_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: '') + project2 = build(:empty_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://') + project2 = build(:empty_project, import_url: 'test://') expect(project2.import_data).to be_nil end + + describe 'project pending deletion' do + let!(:project_pending_deletion) do + create(:empty_project, + pending_delete: true) + end + let(:new_project) do + build(:empty_project, + name: project_pending_deletion.name, + namespace: project_pending_deletion.namespace) + end + + before do + new_project.validate + end + + it 'contains errors related to the project being deleted' do + expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.') + end + end end describe 'default_scope' do @@ -360,14 +382,6 @@ describe Project, models: true do end end - describe "#web_url_without_protocol" do - let(:project) { create(:empty_project, path: "somewhere") } - - it 'returns the web URL without the protocol for this repo' do - expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere") - end - end - describe "#new_issue_address" do let(:project) { create(:empty_project, path: "somewhere") } let(:user) { create(:user) } @@ -1457,6 +1471,18 @@ describe Project, models: true do end end + describe '#gitlab_project_import?' do + subject(:project) { build(:project, import_type: 'gitlab_project') } + + it { expect(project.gitlab_project_import?).to be true } + end + + describe '#gitea_import?' do + subject(:project) { build(:project, import_type: 'gitea') } + + it { expect(project.gitea_import?).to be true } + end + describe '#lfs_enabled?' do let(:project) { create(:project) } @@ -1519,11 +1545,13 @@ describe Project, models: true do end end - describe 'change_head' do + describe '#change_head' do let(:project) { create(:project) } - it 'calls the before_change_head method' do + it 'calls the before_change_head and after_change_head methods' do expect(project.repository).to receive(:before_change_head) + expect(project.repository).to receive(:after_change_head) + project.change_head(project.default_branch) end @@ -1539,11 +1567,6 @@ describe Project, models: true do project.change_head(project.default_branch) end - it 'expires the avatar cache' do - expect(project.repository).to receive(:expire_avatar_cache) - project.change_head(project.default_branch) - end - it 'reloads the default branch' do expect(project).to receive(:reload_default_branch) project.change_head(project.default_branch) @@ -1696,6 +1719,46 @@ describe Project, models: true do end end + describe '#deployment_variables' do + context 'when project has no deployment service' do + let(:project) { create(:empty_project) } + + it 'returns an empty array' do + expect(project.deployment_variables).to eq [] + end + end + + context 'when project has a deployment service' do + let(:project) { create(:kubernetes_project) } + + it 'returns variables from this service' do + expect(project.deployment_variables).to include( + { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false } + ) + end + end + end + + describe '#update_project_statistics' do + let(:project) { create(:empty_project) } + + it "is called after creation" do + expect(project.statistics).to be_a ProjectStatistics + expect(project.statistics).to be_persisted + end + + it "copies the namespace_id" do + expect(project.statistics.namespace_id).to eq project.namespace_id + end + + it "updates the namespace_id when changed" do + namespace = create(:namespace) + project.update(namespace: namespace) + + expect(project.statistics.namespace_id).to eq namespace.id + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ff29f6f66ba425019d87f9ab019f9b5fef803a91 --- /dev/null +++ b/spec/models/project_statistics_spec.rb @@ -0,0 +1,160 @@ +require 'rails_helper' + +describe ProjectStatistics, models: true do + let(:project) { create :empty_project } + let(:statistics) { project.statistics } + + describe 'constants' do + describe 'STORAGE_COLUMNS' do + it 'is an array of symbols' do + expect(described_class::STORAGE_COLUMNS).to be_kind_of Array + expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol] + end + end + + describe 'STATISTICS_COLUMNS' do + it 'is an array of symbols' do + expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array + expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol] + end + + it 'includes all storage columns' do + expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS + end + end + end + + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:namespace) } + end + + describe 'statistics columns' do + it "support values up to 8 exabytes" do + statistics.update!( + commit_count: 8.exabytes - 1, + repository_size: 2.exabytes, + lfs_objects_size: 2.exabytes, + build_artifacts_size: 4.exabytes - 1, + ) + + statistics.reload + + expect(statistics.commit_count).to eq(8.exabytes - 1) + expect(statistics.repository_size).to eq(2.exabytes) + expect(statistics.lfs_objects_size).to eq(2.exabytes) + expect(statistics.build_artifacts_size).to eq(4.exabytes - 1) + expect(statistics.storage_size).to eq(8.exabytes - 1) + end + end + + describe '#total_repository_size' do + it "sums repository and LFS object size" do + statistics.repository_size = 2 + statistics.lfs_objects_size = 3 + statistics.build_artifacts_size = 4 + + expect(statistics.total_repository_size).to eq 5 + end + end + + describe '#refresh!' do + before do + allow(statistics).to receive(:update_commit_count) + allow(statistics).to receive(:update_repository_size) + allow(statistics).to receive(:update_lfs_objects_size) + allow(statistics).to receive(:update_build_artifacts_size) + allow(statistics).to receive(:update_storage_size) + end + + context "without arguments" do + before do + statistics.refresh! + end + + it "sums all counters" do + expect(statistics).to have_received(:update_commit_count) + expect(statistics).to have_received(:update_repository_size) + expect(statistics).to have_received(:update_lfs_objects_size) + expect(statistics).to have_received(:update_build_artifacts_size) + end + end + + context "when passing an only: argument" do + before do + statistics.refresh! only: [:lfs_objects_size] + end + + it "only updates the given columns" do + expect(statistics).to have_received(:update_lfs_objects_size) + expect(statistics).not_to have_received(:update_commit_count) + expect(statistics).not_to have_received(:update_repository_size) + expect(statistics).not_to have_received(:update_build_artifacts_size) + end + end + end + + describe '#update_commit_count' do + before do + allow(project.repository).to receive(:commit_count).and_return(23) + statistics.update_commit_count + end + + it "stores the number of commits in the repository" do + expect(statistics.commit_count).to eq 23 + end + end + + describe '#update_repository_size' do + before do + allow(project.repository).to receive(:size).and_return(12) + statistics.update_repository_size + end + + it "stores the size of the repository" do + expect(statistics.repository_size).to eq 12.megabytes + end + end + + describe '#update_lfs_objects_size' do + let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) } + let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) } + let!(:lfs_objects_project1) { create(:lfs_objects_project, project: project, lfs_object: lfs_object1) } + let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) } + + before do + statistics.update_lfs_objects_size + end + + it "stores the size of related LFS objects" do + expect(statistics.lfs_objects_size).to eq 57.megabytes + end + end + + describe '#update_build_artifacts_size' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build1) { create(:ci_build, pipeline: pipeline, artifacts_size: 45.megabytes) } + let!(:build2) { create(:ci_build, pipeline: pipeline, artifacts_size: 56.megabytes) } + + before do + statistics.update_build_artifacts_size + end + + it "stores the size of related build artifacts" do + expect(statistics.build_artifacts_size).to eq 101.megabytes + end + end + + describe '#update_storage_size' do + it "sums all storage counters" do + statistics.update!( + repository_size: 2, + lfs_objects_size: 3, + ) + + statistics.reload + + expect(statistics.storage_size).to eq 5 + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b5a42edd192b7fa0bd261af7506c30913cdf75e9..99ca53938c8f38518a3d015e4abf163bdfea4848 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -139,6 +139,22 @@ describe Repository, models: true do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end + describe '#last_commit_id_for_path' do + subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } + + it "returns last commit id for a given path" do + is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + end + + it "caches last commit id for a given path" do + cache = repository.send(:cache) + key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" + + expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') + is_expected.to eq('c1acaa5') + end + end + describe '#find_commits_by_message' do it 'returns commits with messages containing a given string' do commit_ids = repository.find_commits_by_message('submodule').map(&:id) @@ -1134,6 +1150,24 @@ describe Repository, models: true do end end + describe '#after_change_head' do + it 'flushes the readme cache' do + expect(repository).to receive(:expire_method_caches).with([ + :readme, + :changelog, + :license, + :contributing, + :version, + :gitignore, + :koding, + :gitlab_ci, + :avatar + ]) + + repository.after_change_head + end + end + describe '#before_push_tag' do it 'flushes the cache' do expect(repository).to receive(:expire_statistics_caches) @@ -1497,14 +1531,6 @@ describe Repository, models: true do end end - describe '#expire_avatar_cache' do - it 'expires the cache' do - expect(repository).to receive(:expire_method_caches).with(%i(avatar)) - - repository.expire_avatar_cache - end - end - describe '#file_on_head' do context 'with a non-existing repository' do it 'returns nil' do diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 6f491fdf9a0a298eedd8dd522670fbcea4aaccd1..8481a9bef1632a81f6126b9de20798fb700ef043 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Route, models: true do - let!(:group) { create(:group) } + let!(:group) { create(:group, path: 'gitlab') } let!(:route) { group.route } describe 'relationships' do @@ -17,13 +17,15 @@ describe Route, models: true do describe '#rename_children' do let!(:nested_group) { create(:group, path: "test", parent: group) } let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) } + let!(:similar_group) { create(:group, path: 'gitlab-org') } - it "updates children routes with new path" do - route.update_attributes(path: 'bar') + before { route.update_attributes(path: 'bar') } + it "updates children routes with new path" do expect(described_class.exists?(path: 'bar')).to be_truthy expect(described_class.exists?(path: 'bar/test')).to be_truthy expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy + expect(described_class.exists?(path: 'gitlab-org')).to be_truthy end end end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f08935b64254c7154702f9ca767c0ba2f913e1c3 --- /dev/null +++ b/spec/models/timelog_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe Timelog, type: :model do + subject { build(:timelog) } + + it { is_expected.to be_valid } + + it { is_expected.to validate_presence_of(:time_spent) } + it { is_expected.to validate_presence_of(:user) } +end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5c34ff041529f6c254fa8018f05b1ab200bb4b81 --- /dev/null +++ b/spec/policies/group_policy_spec.rb @@ -0,0 +1,174 @@ +require 'spec_helper' + +describe GroupPolicy, models: true do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:group) { create(:group) } + + let(:master_permissions) do + [ + :create_projects, + :admin_milestones, + :admin_label + ] + end + + let(:owner_permissions) do + [ + :admin_group, + :admin_namespace, + :admin_group_member, + :change_visibility_level + ] + end + + before do + group.add_guest(guest) + group.add_reporter(reporter) + group.add_developer(developer) + group.add_master(master) + group.add_owner(owner) + end + + subject { described_class.abilities(current_user, group).to_set } + + context 'with no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'guests' do + let(:current_user) { guest } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'reporter' do + let(:current_user) { reporter } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'developer' do + let(:current_user) { developer } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'master' do + let(:current_user) { master } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'owner' do + let(:current_user) { owner } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + context 'admin' do + let(:current_user) { admin } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + describe 'private nested group inherit permissions' do + let(:nested_group) { create(:group, :private, parent: group) } + + subject { described_class.abilities(current_user, nested_group).to_set } + + context 'with no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'guests' do + let(:current_user) { guest } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'reporter' do + let(:current_user) { reporter } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'developer' do + let(:current_user) { developer } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'master' do + let(:current_user) { master } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'owner' do + let(:current_user) { owner } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 964cded917cbf505b08b1b5bd5e17e956441039a..7f8ea5251f0dabbc8bb48993d3e1eff2c937345b 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -146,6 +146,16 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end + + context 'with project path in URL' do + let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + + it 'a new file in project repo' do + post api(url, user), valid_c_params + + expect(response).to have_http_status(201) + end + end end context :delete do diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 5262a623761545d84291e4672de894640a6957b6..bd9ecaf26856c99ed80de5920f426786efbddd80 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -5,7 +5,7 @@ describe API::API, api: true do let!(:user) { create(:user) } let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } - let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id } + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } describe "when unauthenticated" do it "returns authentication success" do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 126496c43a5bbf5656859447372d6c274de20b54..b9d535bc314c7b33fb52399102d58078245135e7 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -46,6 +46,7 @@ describe API::Environments, api: true do expect(response).to have_http_status(201) expect(json_response['name']).to eq('mepmep') + expect(json_response['slug']).to eq('mepmep') expect(json_response['external']).to be nil end @@ -60,6 +61,13 @@ describe API::Environments, api: true do expect(response).to have_http_status(400) end + + it 'returns a 400 if slug is specified' do + post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end end context 'a non member' do @@ -86,6 +94,15 @@ describe API::Environments, api: true do expect(json_response['external_url']).to eq(url) end + it "won't allow slug to be changed" do + slug = environment.slug + api_url = api("/projects/#{project.id}/environments/#{environment.id}", user) + put api_url, slug: slug + "-foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end + it "won't update the external_url if only the name is passed" do url = environment.external_url put api("/projects/#{project.id}/environments/#{environment.id}", user), diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 2081f80ccc14f761bc328d5413afa90221624b21..685da28c6737c41936697b62cdf1a12a531ebd38 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -4,7 +4,14 @@ describe API::Files, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let(:file_path) { 'files/ruby/popen.rb' } + let(:params) do + { + file_path: file_path, + ref: 'master' + } + end let(:author_email) { FFaker::Internet.email } # I have to remove periods from the end of the name @@ -24,36 +31,72 @@ describe API::Files, api: true do before { project.team << [user, :developer] } describe "GET /projects/:id/repository/files" do - it "returns file info" do - params = { - file_path: file_path, - ref: 'master', - } + let(:route) { "/projects/#{project.id}/repository/files" } - get api("/projects/#{project.id}/repository/files", user), params + shared_examples_for 'repository files' do + it "returns file info" do + get api(route, current_user), params - expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq('popen.rb') - expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") - end + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq('popen.rb') + expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") + end - it "returns a 400 bad request if no params given" do - get api("/projects/#{project.id}/repository/files", user) + context 'when no params are given' do + it_behaves_like '400 response' do + let(:request) { get api(route, current_user) } + end + end - expect(response).to have_http_status(400) + context 'when file_path does not exist' do + let(:params) do + { + file_path: 'app/models/application.rb', + ref: 'master', + } + end + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user), params } + let(:message) { '404 File Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user), params } + end + end end - it "returns a 404 if such file does not exist" do - params = { - file_path: 'app/models/application.rb', - ref: 'master', - } + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository files' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end - get api("/projects/#{project.id}/repository/files", user), params + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route), params } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository files' do + let(:current_user) { user } + end + end - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest), params } + end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index a75ba824e85412bef0b39726108de4e5e3a62ade..e355d5e28bcc9006d4c8b08a33d22c79dac4171a 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' describe API::Groups, api: true do include ApiHelpers + include UploadHelpers let(:user1) { create(:user, can_create_group: false) } let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) } + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } let!(:group2) { create(:group, :private) } let!(:project1) { create(:project, namespace: group1) } let!(:project2) { create(:project, namespace: group2) } @@ -23,6 +23,7 @@ describe API::Groups, api: true do context "when unauthenticated" do it "returns authentication error" do get api("/groups") + expect(response).to have_http_status(401) end end @@ -30,20 +31,55 @@ describe API::Groups, api: true do context "when authenticated as user" do it "normal user: returns an array of groups of user1" do get api("/groups", user1) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(group1.name) end + + it "does not include statistics" do + get api("/groups", user1), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include 'statistics' + end end context "when authenticated as admin" do it "admin: returns an array of all groups" do get api("/groups", admin) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) end + + it "does not include statistics by default" do + get api("/groups", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + attributes = { + storage_size: 702, + repository_size: 123, + lfs_objects_size: 234, + build_artifacts_size: 345, + } + + project1.statistics.update!(attributes) + + get api("/groups", admin), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['statistics']).to eq attributes.stringify_keys + end end context "when using skip_groups in request" do @@ -61,6 +97,7 @@ describe API::Groups, api: true do it "returns all groups you have access to" do public_group = create :group, :public + get api("/groups", user1), all_available: true expect(response).to have_http_status(200) @@ -107,6 +144,7 @@ describe API::Groups, api: true do context 'when unauthenticated' do it 'returns authentication error' do get api('/groups/owned') + expect(response).to have_http_status(401) end end @@ -114,6 +152,7 @@ describe API::Groups, api: true do context 'when authenticated as group owner' do it 'returns an array of groups the user owns' do get api('/groups/owned', user2) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['name']).to eq(group2.name) @@ -146,6 +185,7 @@ describe API::Groups, api: true do it "does not return a non existing group" do get api("/groups/1328", user1) + expect(response).to have_http_status(404) end @@ -159,12 +199,14 @@ describe API::Groups, api: true do context "when authenticated as admin" do it "returns any existing group" do get api("/groups/#{group2.id}", admin) + expect(response).to have_http_status(200) expect(json_response['name']).to eq(group2.name) end it "does not return a non existing group" do get api("/groups/1328", admin) + expect(response).to have_http_status(404) end end @@ -172,12 +214,14 @@ describe API::Groups, api: true do context 'when using group path in URL' do it 'returns any existing group' do get api("/groups/#{group1.path}", admin) + expect(response).to have_http_status(200) expect(json_response['name']).to eq(group1.name) end it 'does not return a non existing group' do get api('/groups/unknown', admin) + expect(response).to have_http_status(404) end @@ -269,6 +313,7 @@ describe API::Groups, api: true do it "does not return a non existing group" do get api("/groups/1328/projects", user1) + expect(response).to have_http_status(404) end @@ -292,6 +337,7 @@ describe API::Groups, api: true do context "when authenticated as admin" do it "should return any existing group" do get api("/groups/#{group2.id}/projects", admin) + expect(response).to have_http_status(200) expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project2.name) @@ -299,6 +345,7 @@ describe API::Groups, api: true do it "should not return a non existing group" do get api("/groups/1328/projects", admin) + expect(response).to have_http_status(404) end end @@ -314,6 +361,7 @@ describe API::Groups, api: true do it 'does not return a non existing group' do get api('/groups/unknown/projects', admin) + expect(response).to have_http_status(404) end @@ -329,6 +377,7 @@ describe API::Groups, api: true do context "when authenticated as user without group permissions" do it "does not create group" do post api("/groups", user1), attributes_for(:group) + expect(response).to have_http_status(403) end end @@ -338,6 +387,7 @@ describe API::Groups, api: true do group = attributes_for(:group, { request_access_enabled: false }) post api("/groups", user3), group + expect(response).to have_http_status(201) expect(json_response["name"]).to eq(group[:name]) @@ -347,17 +397,20 @@ describe API::Groups, api: true do it "does not create group, duplicate" do post api("/groups", user3), { name: 'Duplicate Test', path: group2.path } + expect(response).to have_http_status(400) expect(response.message).to eq("Bad Request") end it "returns 400 bad request error if name not given" do post api("/groups", user3), { path: group2.path } + expect(response).to have_http_status(400) end it "returns 400 bad request error if path not given" do post api("/groups", user3), { name: 'test' } + expect(response).to have_http_status(400) end end @@ -367,18 +420,22 @@ describe API::Groups, api: true do context "when authenticated as user" do it "removes group" do delete api("/groups/#{group1.id}", user1) + expect(response).to have_http_status(200) end it "does not remove a group if not an owner" do user4 = create(:user) group1.add_master(user4) + delete api("/groups/#{group1.id}", user3) + expect(response).to have_http_status(403) end it "does not remove a non existing group" do delete api("/groups/1328", user1) + expect(response).to have_http_status(404) end @@ -392,11 +449,13 @@ describe API::Groups, api: true do context "when authenticated as admin" do it "removes any existing group" do delete api("/groups/#{group2.id}", admin) + expect(response).to have_http_status(200) end it "does not remove a non existing group" do delete api("/groups/1328", admin) + expect(response).to have_http_status(404) end end @@ -404,15 +463,17 @@ describe API::Groups, api: true do describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:project) } + let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + before(:each) do allow_any_instance_of(Projects::TransferService). to receive(:execute).and_return(true) - allow(Project).to receive(:find).and_return(project) end context "when authenticated as user" do it "does not transfer project to group" do post api("/groups/#{group1.id}/projects/#{project.id}", user2) + expect(response).to have_http_status(403) end end @@ -420,8 +481,45 @@ describe API::Groups, api: true do context "when authenticated as admin" do it "transfers project to group" do post api("/groups/#{group1.id}/projects/#{project.id}", admin) + expect(response).to have_http_status(201) end + + context 'when using project path in URL' do + context 'with a valid project path' do + it "transfers project to group" do + post api("/groups/#{group1.id}/projects/#{project_path}", admin) + + expect(response).to have_http_status(201) + end + end + + context 'with a non-existent project path' do + it "does not transfer project to group" do + post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin) + + expect(response).to have_http_status(404) + end + end + end + + context 'when using a group path in URL' do + context 'with a valid group path' do + it "transfers project to group" do + post api("/groups/#{group1.path}/projects/#{project_path}", admin) + + expect(response).to have_http_status(201) + end + end + + context 'with a non-existent group path' do + it "does not transfer project to group" do + post api("/groups/noexist/projects/#{project_path}", admin) + + expect(response).to have_http_status(404) + end + end + end end end end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 4035fd97af59a52ff0eafccb8bfaf25d12774912..b8ee2293a335f08bfbbfcb999018cb6d95da6856 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe API::Helpers, api: true do + include API::APIGuard::HelperMethods include API::Helpers include SentryHelper @@ -15,24 +16,24 @@ describe API::Helpers, api: true do def set_env(user_or_token, identifier) clear_env clear_param - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token env[API::Helpers::SUDO_HEADER] = identifier.to_s end def set_param(user_or_token, identifier) clear_env clear_param - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token params[API::Helpers::SUDO_PARAM] = identifier.to_s end def clear_env - env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER) env.delete(API::Helpers::SUDO_HEADER) end def clear_param - params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) + params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM) params.delete(API::Helpers::SUDO_PARAM) end @@ -94,22 +95,28 @@ describe API::Helpers, api: true do describe "when authenticating using a user's private token" do it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil end it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil end it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token + + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token + expect(current_user).to eq(user) end end @@ -117,37 +124,51 @@ describe API::Helpers, api: true do describe "when authenticating using a user's personal access tokens" do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } + end + it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' + expect(current_user).to be_nil end it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + + expect(current_user).to be_nil + end + + it "returns nil for a token without the appropriate scope" do + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_access_with_scope('write_user') + expect(current_user).to be_nil end it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token + expect(current_user).to eq(user) end it 'does not allow revoked tokens' do personal_access_token.revoke! - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to be_nil end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to be_nil end end @@ -375,7 +396,7 @@ describe API::Helpers, api: true do %w[HEAD GET].each do |method_name| context "method is #{method_name}" do before do - expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name)) end it 'does not raise an error' do @@ -389,7 +410,7 @@ describe API::Helpers, api: true do %w[POST PUT PATCH DELETE].each do |method_name| context "method is #{method_name}" do before do - expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name)) end it 'calls authenticate!' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5c80dd98dc7bfe25f68819587bfd5a3655ae5ebb..807c999b84aad1e422377dcfd1ee90f5d807c4bc 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -50,6 +50,8 @@ describe API::Issues, api: true do end let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + let(:no_milestone_title) { URI.escape(Milestone::None.title) } + before do project.team << [user, :reporter] project.team << [guest, :guest] @@ -107,6 +109,7 @@ describe API::Issues, api: true do it 'returns an array of labeled issues when at least one label matches' do get api("/issues?labels=#{label.title},foo,bar", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -136,6 +139,51 @@ describe API::Issues, api: true do expect(json_response.length).to eq(0) end + it 'returns an empty array if no issue matches milestone' do + get api("/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get api("/issues?milestone=foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get api("/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get api("/issues?milestone=#{milestone.title}"\ + '&state=closed', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get api("/issues?milestone=#{no_milestone_title}", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api('/issues', user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -318,6 +366,15 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(group_closed_issue.id) end + it 'returns an array of issues with no milestone' do + get api("#{base_url}?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api(base_url, user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -357,7 +414,6 @@ describe API::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } - let(:title) { milestone.title } it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) @@ -433,8 +489,9 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled project issues when at least one label matches' do + it 'returns an array of labeled project issues where all labels match' do get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -463,7 +520,8 @@ describe API::Issues, api: true do end it 'returns an array of issues in given milestone' do - get api("#{base_url}/issues?milestone=#{title}", user) + get api("#{base_url}/issues?milestone=#{milestone.title}", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -480,6 +538,15 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(closed_issue.id) end + it 'returns an array of issues with no milestone' do + get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -547,12 +614,21 @@ describe API::Issues, api: true do it 'returns a project issue by iid' do get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) + expect(response.status).to eq 200 + expect(json_response.length).to eq 1 expect(json_response.first['title']).to eq issue.title expect(json_response.first['id']).to eq issue.id expect(json_response.first['iid']).to eq issue.iid end + it 'returns an empty array for an unknown project issue iid' do + get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 0 + end + it "returns 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) expect(response).to have_http_status(404) @@ -932,6 +1008,13 @@ describe API::Issues, api: true do expect(json_response['state']).to eq "closed" end + it 'reopens a project isssue' do + put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq 'reopened' + end + context 'when an admin or owner makes the request' do it 'accepts the update date to be set' do update_time = 2.weeks.ago @@ -1110,4 +1193,10 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end end + + describe 'time tracking endpoints' do + let(:issuable) { issue } + + include_examples 'time tracking endpoints', 'issue' + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index f032d1b683dd14b7593cac889f261b918125ff58..4e4fea1dad8abf023a0b8797ea67d63a27a4f147 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -6,7 +6,7 @@ describe API::MergeRequests, api: true do let(:user) { create(:user) } let(:admin) { create(:user, :admin) } let(:non_member) { create(:user) } - let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } @@ -671,6 +671,12 @@ describe API::MergeRequests, api: true do end end + describe 'Time tracking' do + let(:issuable) { merge_request } + + include_examples 'time tracking endpoints', 'merge_request' + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index a42cedae614cdb91a34ea3f62e0ee46d56da4627..36fbcf088e704a59f93aec32ea7fdecc4e703dab 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -86,7 +86,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do describe "POST /projects/:id/hooks" do it "adds hook to project" do expect do - post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true + post api("/projects/#{project.id}/hooks", user), + url: "http://example.com", issues_events: true, wiki_page_events: true end.to change {project.hooks.count}.by(1) expect(response).to have_http_status(201) @@ -98,7 +99,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['note_events']).to eq(false) expect(json_response['build_events']).to eq(false) expect(json_response['pipeline_events']).to eq(false) - expect(json_response['wiki_page_events']).to eq(false) + expect(json_response['wiki_page_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true) expect(json_response).not_to include('token') end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c5d67a90abc194813a123721399e7a26d5867977..cdb16b4c46b9dacaed44cf4aedb3039ee93e3de7 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -49,7 +49,7 @@ describe API::Projects, api: true do end end - context 'when authenticated' do + context 'when authenticated as regular user' do it 'returns an array of projects' do get api('/projects', user) expect(response).to have_http_status(200) @@ -167,11 +167,27 @@ describe API::Projects, api: true do expect(json_response).to satisfy do |response| response.one? do |entry| entry.has_key?('permissions') && - entry['name'] == project.name && + entry['name'] == project.name && entry['owner']['username'] == user.username end end end + + it "does not include statistics by default" do + get api('/projects/all', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + get api('/projects/all', admin), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).to include 'statistics' + end end end @@ -196,6 +212,32 @@ describe API::Projects, api: true do expect(json_response.first['name']).to eq(project4.name) expect(json_response.first['owner']['username']).to eq(user4.username) end + + it "does not include statistics by default" do + get api('/projects/owned', user4) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + attributes = { + commit_count: 23, + storage_size: 702, + repository_size: 123, + lfs_objects_size: 234, + build_artifacts_size: 345, + } + + project4.statistics.update!(attributes) + + get api('/projects/owned', user4), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['statistics']).to eq attributes.stringify_keys + end end end @@ -630,6 +672,18 @@ describe API::Projects, api: true do expect(json_response['name']).to eq(project.name) end + it 'exposes namespace fields' do + get api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['namespace']).to eq({ + 'id' => user.namespace.id, + 'name' => user.namespace.name, + 'path' => user.namespace.path, + 'kind' => user.namespace.kind, + }) + end + describe 'permissions' do context 'all projects' do before { project.team << [user, :master] } @@ -1031,7 +1085,7 @@ describe API::Projects, api: true do end describe 'GET /projects/search/:query' do - let!(:query) { 'query'} + let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } @@ -1041,32 +1095,37 @@ describe API::Projects, api: true do let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } + let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") } shared_examples_for 'project search response' do |args = {}| it 'returns project search responses' do - get api("/projects/search/#{query}", current_user) + get api("/projects/search/#{args[:query]}", current_user) expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq(args[:results]) - json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) } + json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } end end context 'when unauthenticated' do - it_behaves_like 'project search response', results: 1 do + it_behaves_like 'project search response', query: 'query', results: 1 do let(:current_user) { nil } end end context 'when authenticated' do - it_behaves_like 'project search response', results: 6 do + it_behaves_like 'project search response', query: 'query', results: 6 do + let(:current_user) { user } + end + it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do let(:current_user) { user } end + end context 'when authenticated as a different user' do - it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do + it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do let(:current_user) { user2 } end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index c90b69e8ebbcc69edbdf7d21af5d8b65aed7698c..0b19fa38c555ea4b41f6b411a9a67f0d3064199f 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -7,204 +7,415 @@ describe API::Repositories, api: true do include WorkhorseHelpers let(:user) { create(:user) } - let(:user2) { create(:user) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let!(:project) { create(:project, creator_id: user.id) } let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } describe "GET /projects/:id/repository/tree" do - context "authorized user" do - before { project.team << [user2, :reporter] } + let(:route) { "/projects/#{project.id}/repository/tree" } - it "returns project commits" do - get api("/projects/#{project.id}/repository/tree", user) + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get api(route, current_user) expect(response).to have_http_status(200) + first_commit = json_response.first + expect(json_response).to be_an Array - expect(json_response.first['name']).to eq('bar') - expect(json_response.first['type']).to eq('tree') - expect(json_response.first['mode']).to eq('040000') + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end end - it 'returns a 404 for unknown ref' do - get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) - expect(response).to have_http_status(404) + context 'when repository is disabled' do + include_context 'disabled repository' - expect(json_response).to be_an Object - json_response['message'] == '404 Tree Not Found' + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'with recursive=1' do + it 'returns recursive project paths tree' do + get api("#{route}?recursive=1", current_user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response[4]['name']).to eq('html') + expect(json_response[4]['path']).to eq('files/html') + expect(json_response[4]['type']).to eq('tree') + expect(json_response[4]['mode']).to eq('040000') + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end end end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project.id}/repository/tree") - expect(response).to have_http_status(401) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } end end - end - describe 'GET /projects/:id/repository/tree?recursive=1' do - context 'authorized user' do - before { project.team << [user2, :reporter] } + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end + end - it 'should return recursive project paths tree' do - get api("/projects/#{project.id}/repository/tree?recursive=1", user) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end + end + end - expect(response.status).to eq(200) + { + 'blobs/:sha' => 'blobs/master', + 'commits/:sha/blob' => 'commits/master/blob' + }.each do |desc_path, example_path| + describe "GET /projects/:id/repository/#{desc_path}" do + let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } + + shared_examples_for 'repository blob' do + it 'returns the repository blob' do + get api(route, current_user) + + expect(response).to have_http_status(200) + end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:message) { '404 Commit Not Found' } + end + end + + context 'when filepath does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) } + let(:message) { '404 File Not Found' } + end + end + + context 'when no filepath is given' do + it_behaves_like '400 response' do + let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + end - expect(json_response).to be_an Array - expect(json_response[4]['name']).to eq('html') - expect(json_response[4]['path']).to eq('files/html') - expect(json_response[4]['type']).to eq('tree') - expect(json_response[4]['mode']).to eq('040000') + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it 'returns a 404 for unknown ref' do - get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end - expect(json_response).to be_an Object - json_response['message'] == '404 Tree Not Found' + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository blob' do + let(:current_user) { user } + end end - end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project.id}/repository/tree?recursive=1") - expect(response).to have_http_status(401) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end end - describe "GET /projects/:id/repository/blobs/:sha" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user) - expect(response).to have_http_status(200) + describe "GET /projects/:id/repository/raw_blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + + shared_examples_for 'repository raw blob' do + it 'returns the repository raw blob' do + get api(route, current_user) + + expect(response).to have_http_status(200) + end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - it "returns 404 for invalid branch_name" do - get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository raw blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "returns 404 for invalid file" do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "returns a 400 error if filepath is missing" do - get api("/projects/#{project.id}/repository/blobs/master", user) - expect(response).to have_http_status(400) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository raw blob' do + let(:current_user) { user } + end end - end - describe "GET /projects/:id/repository/commits/:sha/blob" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) - expect(response).to have_http_status(200) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user) - expect(response).to have_http_status(200) - end + describe "GET /projects/:id/repository/archive(.:format)?:sha" do + let(:route) { "/projects/#{project.id}/repository/archive" } + + shared_examples_for 'repository archive' do + it 'returns the repository archive' do + get api(route, current_user) + + expect(response).to have_http_status(200) - it 'returns a 404 for unknown blob' do - get api("/projects/#{project.id}/repository/raw_blobs/123456", user) - expect(response).to have_http_status(404) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data - expect(json_response).to be_an Object - json_response['message'] == '404 Blob Not Found' + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + end + + it 'returns the repository archive archive.zip' do + get api("/projects/#{project.id}/repository/archive.zip", user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + end + + it 'returns the repository archive archive.tar.bz2' do + get api("/projects/#{project.id}/repository/archive.tar.bz2", user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?sha=xxx", current_user) } + let(:message) { '404 File Not Found' } + end + end end - end - describe "GET /projects/:id/repository/archive(.:format)?:sha" do - it "gets the archive" do - get api("/projects/#{project.id}/repository/archive", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository archive' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "gets the archive.zip" do - get api("/projects/#{project.id}/repository/archive.zip", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "gets the archive.tar.bz2" do - get api("/projects/#{project.id}/repository/archive.tar.bz2", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository archive' do + let(:current_user) { user } + end end - it "returns 404 for invalid sha" do - get api("/projects/#{project.id}/repository/archive/?sha=xxx", user) - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end describe 'GET /projects/:id/repository/compare' do - it "compares branches" do - get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + let(:route) { "/projects/#{project.id}/repository/compare" } + + shared_examples_for 'repository compare' do + it "compares branches" do + get api(route, current_user), from: 'master', to: 'feature' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares tags" do + get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares commits" do + get api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey + end + + it "compares commits in reverse order" do + get api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares same refs" do + get api(route, current_user), from: 'master', to: 'master' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy + end end - it "compares tags" do - get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository compare' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "compares commits" do - get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_falsey + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "compares commits in reverse order" do - get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository compare' do + let(:current_user) { user } + end end - it "compares same refs" do - get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_truthy + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end describe 'GET /projects/:id/repository/contributors' do - it 'returns valid data' do - get api("/projects/#{project.id}/repository/contributors", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - contributor = json_response.first - expect(contributor['email']).to eq('tiagonbotelho@hotmail.com') - expect(contributor['name']).to eq('tiagonbotelho') - expect(contributor['commits']).to eq(1) - expect(contributor['additions']).to eq(0) - expect(contributor['deletions']).to eq(0) + let(:route) { "/projects/#{project.id}/repository/contributors" } + + shared_examples_for 'repository contributors' do + it 'returns valid data' do + get api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_contributor = json_response.first + + expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') + expect(first_contributor['name']).to eq('tiagonbotelho') + expect(first_contributor['commits']).to eq(1) + expect(first_contributor['additions']).to eq(0) + expect(first_contributor['deletions']).to eq(0) + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository contributors' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository contributors' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 668e39f9dba983a4e2fe3d7f51543ed1079d9b18..39c9e0505d1a9c200c42eae28ba9f15294c30d05 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -6,7 +6,7 @@ describe API::Services, api: true do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:user2) { create(:user) } - let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } + let(:project) {create(:empty_project, creator_id: user.id, namespace: user.namespace) } Service.available_services_names.each do |service| describe "PUT /projects/:id/services/#{service.dasherize}" do @@ -92,57 +92,78 @@ describe API::Services, api: true do describe 'POST /projects/:id/services/:slug/trigger' do let!(:project) { create(:empty_project) } - let(:service_name) { 'mattermost_slash_commands' } - context 'no service is available' do - it 'returns a not found message' do - post api("/projects/#{project.id}/services/idonotexist/trigger") + describe 'Mattermost Service' do + let(:service_name) { 'mattermost_slash_commands' } - expect(response).to have_http_status(404) - expect(json_response["error"]).to eq("404 Not Found") + context 'no service is available' do + it 'returns a not found message' do + post api("/projects/#{project.id}/services/idonotexist/trigger") + + expect(response).to have_http_status(404) + expect(json_response["error"]).to eq("404 Not Found") + end end - end - context 'the service exists' do - let(:params) { { token: 'token' } } + context 'the service exists' do + let(:params) { { token: 'token' } } - context 'the service is not active' do - let!(:inactive_service) do - project.create_mattermost_slash_commands_service( - active: false, - properties: { token: 'token' } - ) - end + context 'the service is not active' do + before do + project.create_mattermost_slash_commands_service( + active: false, + properties: params + ) + end - it 'when the service is inactive' do - post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params + it 'when the service is inactive' do + post api("/projects/#{project.id}/services/#{service_name}/trigger"), params - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end - end - context 'the service is active' do - let!(:active_service) do - project.create_mattermost_slash_commands_service( - active: true, - properties: { token: 'token' } - ) + context 'the service is active' do + before do + project.create_mattermost_slash_commands_service( + active: true, + properties: params + ) + end + + it 'returns status 200' do + post api("/projects/#{project.id}/services/#{service_name}/trigger"), params + + expect(response).to have_http_status(200) + end end - it 'returns status 200' do - post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params + context 'when the project can not be found' do + it 'returns a generic 404' do + post api("/projects/404/services/#{service_name}/trigger"), params - expect(response).to have_http_status(200) + expect(response).to have_http_status(404) + expect(json_response["message"]).to eq("404 Service Not Found") + end end end + end - context 'when the project can not be found' do - it 'returns a generic 404' do - post api("/projects/404/services/mattermost_slash_commands/trigger"), params + describe 'Slack Service' do + let(:service_name) { 'slack_slash_commands' } - expect(response).to have_http_status(404) - expect(json_response["message"]).to eq("404 Service Not Found") - end + before do + project.create_slack_slash_commands_service( + active: true, + properties: { token: 'token' } + ) + end + + it 'returns status 200' do + post api("/projects/#{project.id}/services/#{service_name}/trigger"), token: 'token', text: 'help' + + expect(response).to have_http_status(200) + expect(json_response['response_type']).to eq("ephemeral") end end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 9a8d633d657a6a7ad2dabab9dcbd34f235b6e4a8..91e3c333a02e63fa8402e582cb7d2c7d68086910 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -16,6 +16,8 @@ describe API::Settings, 'Settings', api: true do expect(json_response['repository_storage']).to eq('default') expect(json_response['koding_enabled']).to be_falsey expect(json_response['koding_url']).to be_nil + expect(json_response['plantuml_enabled']).to be_falsey + expect(json_response['plantuml_url']).to be_nil end end @@ -28,7 +30,8 @@ describe API::Settings, 'Settings', api: true do it "updates application settings" do put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com' + default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', + plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -36,6 +39,8 @@ describe API::Settings, 'Settings', api: true do expect(json_response['repository_storages']).to eq(['custom']) expect(json_response['koding_enabled']).to be_truthy expect(json_response['koding_url']).to eq('http://koding.example.com') + expect(json_response['plantuml_enabled']).to be_truthy + expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') end end @@ -44,8 +49,16 @@ describe API::Settings, 'Settings', api: true do put api("/application/settings", admin), koding_enabled: true expect(response).to have_http_status(400) - expect(json_response['message']).to have_key('koding_url') - expect(json_response['message']['koding_url']).to include "can't be blank" + expect(json_response['error']).to eq('koding_url is missing') + end + end + + context "missing plantuml_url value when plantuml_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), plantuml_enabled: true + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('plantuml_url is missing') end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 9e317f3a7e91498abf37ebff32771bdcc972fdf5..5bf5bf0739e4b871c32097239a93a2bf636ff505 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -137,6 +137,15 @@ describe API::Users, api: true do expect(new_user.can_create_group).to eq(true) end + it "creates user with optional attributes" do + optional_attributes = { confirm: true } + attributes = attributes_for(:user).merge(optional_attributes) + + post api('/users', admin), attributes + + expect(response).to have_http_status(201) + end + it "creates non-admin user" do post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false) expect(response).to have_http_status(201) @@ -265,6 +274,14 @@ describe API::Users, api: true do expect(response).to have_http_status(409) expect(json_response['message']).to eq('Username has already been taken') end + + it 'creates user with new identity' do + post api("/users", admin), attributes_for(:user, provider: 'github', extern_uid: '67890') + + expect(response).to have_http_status(201) + expect(json_response['identities'].first['extern_uid']).to eq('67890') + expect(json_response['identities'].first['provider']).to eq('github') + end end end @@ -317,9 +334,9 @@ describe API::Users, api: true do end it 'updates user with new identity' do - put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890' + put api("/users/#{user.id}", admin), provider: 'github', extern_uid: 'john' expect(response).to have_http_status(200) - expect(user.reload.identities.first.extern_uid).to eq('67890') + expect(user.reload.identities.first.extern_uid).to eq('john') expect(user.reload.identities.first.provider).to eq('github') end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8ccae0d5bffc4d7f4dd3f440a9815b9825d38f80..70c94c9a90f0d69f99b866e377312096f60506b4 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -62,6 +62,11 @@ describe Ci::API::Builds do let(:user_agent) { 'Go-http-client/1.1' } it { expect(response).to have_http_status(404) } end + + context "when runner doesn't have a User-Agent" do + let(:user_agent) { nil } + it { expect(response).to have_http_status(404) } + end end context 'when there is a pending build' do @@ -277,7 +282,11 @@ describe Ci::API::Builds do end describe 'PATCH /builds/:id/trace.txt' do - let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) } + let(:build) do + attributes = { runner_id: runner.id, pipeline: pipeline } + create(:ci_build, :running, :trace, attributes) + end + let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } let(:update_interval) { 10.seconds.to_i } @@ -304,7 +313,6 @@ describe Ci::API::Builds do end before do - build.run! initial_patch_the_trace end @@ -357,6 +365,19 @@ describe Ci::API::Builds do end end end + + context 'when project for the build has been deleted' do + let(:build) do + attributes = { runner_id: runner.id, pipeline: pipeline } + create(:ci_build, :running, :trace, attributes) do |build| + build.project.update(pending_delete: true) + end + end + + it 'responds with forbidden' do + expect(response.status).to eq(403) + end + end end context 'when Runner makes a force-patch' do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index f1728d61def1be85ce85a015b154e094b79a8274..5abda28e26fa94b3842d1694c3ade0bc82b23309 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -230,7 +230,7 @@ describe 'Git HTTP requests', lib: true do context "when an oauth token is provided" do before do application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") end it "downloads get status 200" do @@ -371,12 +371,26 @@ describe 'Git HTTP requests', lib: true do shared_examples 'can download code only' do it 'downloads get status 200' do - clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token + allow_any_instance_of(Repository). + to receive(:exists?).and_return(true) + + clone_get "#{project.path_with_namespace}.git", + user: 'gitlab-ci-token', password: build.token expect(response).to have_http_status(200) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end + it 'downloads from non-existing repository and gets 403' do + allow_any_instance_of(Repository). + to receive(:exists?).and_return(false) + + clone_get "#{project.path_with_namespace}.git", + user: 'gitlab-ci-token', password: build.token + + expect(response).to have_http_status(403) + end + it 'uploads get status 403' do push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 69eeb45ed71589744275663176dae332d374d29b..99c44bde1518509ec046d946591ef76260499bc4 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do end it "to #show" do - expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab') + expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce') + expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce') end end @@ -119,3 +120,20 @@ describe Admin::HealthCheckController, "routing" do expect(get("/admin/health_check")).to route_to('admin/health_check#show') end end + +describe Admin::GroupsController, "routing" do + let(:name) { 'complex.group-namegit' } + + it "to #index" do + expect(get("/admin/groups")).to route_to('admin/groups#index') + end + + it "to #show" do + expect(get("/admin/groups/#{name}")).to route_to('admin/groups#show', id: name) + expect(get("/admin/groups/#{name}/subgroup")).to route_to('admin/groups#show', id: "#{name}/subgroup") + end + + it "to #edit" do + expect(get("/admin/groups/#{name}/edit")).to route_to('admin/groups#edit', id: name) + end +end diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..78ff9c6e6fd0a3a9df72b066513f34ce414fc276 --- /dev/null +++ b/spec/routing/import_routing_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +# Shared examples for a resource inside a Project +# +# By default it tests all the default REST actions: index, create, new, edit, +# show, update, and destroy. You can remove actions by customizing the +# `actions` variable. +# +# It also expects a `controller` variable to be available which defines both +# the path to the resource as well as the controller name. +# +# Examples +# +# # Default behavior +# it_behaves_like 'RESTful project resources' do +# let(:controller) { 'issues' } +# end +# +# # Customizing actions +# it_behaves_like 'RESTful project resources' do +# let(:actions) { [:index] } +# let(:controller) { 'issues' } +# end +shared_examples 'importer routing' do + let(:except_actions) { [] } + + it 'to #create' do + expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create) + end + + it 'to #new' do + expect(get("/import/#{provider}/new")).to route_to("import/#{provider}#new") unless except_actions.include?(:new) + end + + it 'to #status' do + expect(get("/import/#{provider}/status")).to route_to("import/#{provider}#status") unless except_actions.include?(:status) + end + + it 'to #callback' do + expect(get("/import/#{provider}/callback")).to route_to("import/#{provider}#callback") unless except_actions.include?(:callback) + end + + it 'to #jobs' do + expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs) + end +end + +# personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token +# status_import_github GET /import/github/status(.:format) import/github#status +# callback_import_github GET /import/github/callback(.:format) import/github#callback +# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs +# import_github POST /import/github(.:format) import/github#create +# new_import_github GET /import/github/new(.:format) import/github#new +describe Import::GithubController, 'routing' do + it_behaves_like 'importer routing' do + let(:provider) { 'github' } + end + + it 'to #personal_access_token' do + expect(post('/import/github/personal_access_token')).to route_to('import/github#personal_access_token') + end +end + +# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token +# status_import_gitea GET /import/gitea/status(.:format) import/gitea#status +# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs +# import_gitea POST /import/gitea(.:format) import/gitea#create +# new_import_gitea GET /import/gitea/new(.:format) import/gitea#new +describe Import::GiteaController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'gitea' } + end + + it 'to #personal_access_token' do + expect(post('/import/gitea/personal_access_token')).to route_to('import/gitea#personal_access_token') + end +end + +# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status +# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback +# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs +# import_gitlab POST /import/gitlab(.:format) import/gitlab#create +describe Import::GitlabController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:new] } + let(:provider) { 'gitlab' } + end +end + +# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status +# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback +# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs +# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create +describe Import::BitbucketController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:new] } + let(:provider) { 'bitbucket' } + end +end + +# status_import_google_code GET /import/google_code/status(.:format) import/google_code#status +# callback_import_google_code POST /import/google_code/callback(.:format) import/google_code#callback +# jobs_import_google_code GET /import/google_code/jobs(.:format) import/google_code#jobs +# new_user_map_import_google_code GET /import/google_code/user_map(.:format) import/google_code#new_user_map +# create_user_map_import_google_code POST /import/google_code/user_map(.:format) import/google_code#create_user_map +# import_google_code POST /import/google_code(.:format) import/google_code#create +# new_import_google_code GET /import/google_code/new(.:format) import/google_code#new +describe Import::GoogleCodeController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'google_code' } + end + + it 'to #callback' do + expect(post("/import/google_code/callback")).to route_to("import/google_code#callback") + end + + it 'to #new_user_map' do + expect(get('/import/google_code/user_map')).to route_to('import/google_code#new_user_map') + end + + it 'to #create_user_map' do + expect(post('/import/google_code/user_map')).to route_to('import/google_code#create_user_map') + end +end + +# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status +# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback +# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs +# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map +# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map +# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create +# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new +describe Import::FogbugzController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'fogbugz' } + end + + it 'to #callback' do + expect(post("/import/fogbugz/callback")).to route_to("import/fogbugz#callback") + end + + it 'to #new_user_map' do + expect(get('/import/fogbugz/user_map')).to route_to('import/fogbugz#new_user_map') + end + + it 'to #create_user_map' do + expect(post('/import/fogbugz/user_map')).to route_to('import/fogbugz#create_user_map') + end +end + +# import_gitlab_project POST /import/gitlab_project(.:format) import/gitlab_projects#create +# POST /import/gitlab_project(.:format) import/gitlab_projects#create +# new_import_gitlab_project GET /import/gitlab_project/new(.:format) import/gitlab_projects#new +describe Import::GitlabProjectsController, 'routing' do + it 'to #create' do + expect(post('/import/gitlab_project')).to route_to('import/gitlab_projects#create') + end + + it 'to #new' do + expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new') + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index b6e7da841b1c7cf1ee18f2a5f8c4757dccc1df5d..77549db29277b5c15a40290f9fd43c8892287d1e 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -80,10 +80,6 @@ describe 'project routing' do expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') end - it 'to #autocomplete_sources' do - expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') - end - describe 'to #show' do context 'regular name' do it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') } @@ -117,6 +113,21 @@ describe 'project routing' do end end + # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis + # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members + # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues + # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests + # labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels + # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones + # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands + describe Projects::AutocompleteSourcesController, 'routing' do + [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| + it "to ##{action}" do + expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') + end + end + end + # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages # history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history # project_wikis POST /:project_id/wikis(.:format) projects/wikis#create diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 6b33fe66a63767944b02cb0f1ecb2a84567806ef..86e703a6448081a5803cde0362accf553976f815 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -13,6 +13,14 @@ describe AnalyticsBuildEntity do subject { entity.as_json } + before do + Timecop.freeze + end + + after do + Timecop.return + end + it 'contains the URL' do expect(subject).to include(:url) end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f99518266830c1349b24bbcb88016811b90d0d1a --- /dev/null +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe AnalyticsStageSerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) } + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'it generates payload for single object' do + expect(json).to be_kind_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :description, :value) + end +end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7a84c8b0b40401da5f60d149e4cd4e350d0b7df3 --- /dev/null +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe AnalyticsSummarySerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:resource) do + Gitlab::CycleAnalytics::Summary::Issue.new(project: double, + from: 1.day.ago, + current_user: user) + end + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) + end + + it 'it generates payload for single object' do + expect(json).to be_kind_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :value) + end +end diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0f7be8b2c39baf569fc220f6da96a9d41663a2f3 --- /dev/null +++ b/spec/serializers/build_action_entity_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe BuildActionEntity do + let(:build) { create(:ci_build, name: 'test_build') } + + let(:entity) do + described_class.new(build, request: double) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains original build name' do + expect(subject[:name]).to eq 'test_build' + end + + it 'contains path to the action play' do + expect(subject[:path]).to include "builds/#{build.id}/play" + end + end +end diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2fc60aa9de660c43bcf11bdcd2aa66633acae710 --- /dev/null +++ b/spec/serializers/build_artifact_entity_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe BuildArtifactEntity do + let(:build) { create(:ci_build, name: 'test:build') } + + let(:entity) do + described_class.new(build, request: double) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains build name' do + expect(subject[:name]).to eq 'test:build' + end + + it 'contains path to the artifacts' do + expect(subject[:path]) + .to include "builds/#{build.id}/artifacts/download" + end + end +end diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb index 15f11ac3df94d7f1c25c8eedc73731972c4e4153..0333d73b5b51351469a6516aec347630ab882e62 100644 --- a/spec/serializers/commit_entity_spec.rb +++ b/spec/serializers/commit_entity_spec.rb @@ -33,10 +33,12 @@ describe CommitEntity do it 'contains path to commit' do expect(subject).to include(:commit_path) + expect(subject[:commit_path]).to include "commit/#{commit.id}" end it 'contains URL to commit' do expect(subject).to include(:commit_url) + expect(subject[:commit_path]).to include "commit/#{commit.id}" end it 'needs to receive project in the request' do @@ -45,4 +47,8 @@ describe CommitEntity do subject end + + it 'exposes gravatar url that belongs to author' do + expect(subject.fetch(:author_gravatar_url)).to match /gravatar/ + end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b19464c7117aefd2390f7cd4b69698b83fef3725 --- /dev/null +++ b/spec/serializers/pipeline_entity_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe PipelineEntity do + let(:user) { create(:user) } + let(:request) { double('request') } + + before do + allow(request).to receive(:user).and_return(user) + end + + let(:entity) do + described_class.represent(pipeline, request: request) + end + + describe '#as_json' do + subject { entity.as_json } + + context 'when pipeline is empty' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'contains required fields' do + expect(subject).to include :id, :user, :path + expect(subject).to include :ref, :commit + expect(subject).to include :updated_at, :created_at + end + + it 'contains details' do + expect(subject).to include :details + expect(subject[:details]) + .to include :duration, :finished_at + expect(subject[:details]) + .to include :stages, :artifacts, :manual_actions + expect(subject[:details][:status]).to include :icon, :text, :label + end + + it 'contains flags' do + expect(subject).to include :flags + expect(subject[:flags]) + .to include :latest, :triggered, :stuck, + :yaml_errors, :retryable, :cancelable + end + end + + context 'when pipeline is retryable' do + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_pipeline, status: :success, project: project) + end + + before do + create(:ci_build, :failed, pipeline: pipeline) + end + + context 'user has ability to retry pipeline' do + before { project.team << [user, :developer] } + + it 'retryable flag is true' do + expect(subject[:flags][:retryable]).to eq true + end + + it 'contains retry path' do + expect(subject[:retry_path]).to be_present + end + end + + context 'user does not have ability to retry pipeline' do + it 'retryable flag is false' do + expect(subject[:flags][:retryable]).to eq false + end + + it 'does not contain retry path' do + expect(subject).not_to have_key(:retry_path) + end + end + end + + context 'when pipeline is cancelable' do + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_pipeline, status: :running, project: project) + end + + before do + create(:ci_build, :pending, pipeline: pipeline) + end + + context 'user has ability to cancel pipeline' do + before { project.team << [user, :developer] } + + it 'cancelable flag is true' do + expect(subject[:flags][:cancelable]).to eq true + end + + it 'contains cancel path' do + expect(subject[:cancel_path]).to be_present + end + end + + context 'user does not have ability to cancel pipeline' do + it 'cancelable flag is false' do + expect(subject[:flags][:cancelable]).to eq false + end + + it 'does not contain cancel path' do + expect(subject).not_to have_key(:cancel_path) + end + end + end + + context 'when pipeline has YAML errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { invalid: :value } }) + end + + it 'contains flag that indicates there are errors' do + expect(subject[:flags][:yaml_errors]).to be true + end + + it 'contains information about error' do + expect(subject[:yaml_errors]).to be_present + end + end + + context 'when pipeline does not have YAML errors' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'contains flag that indicates there are no errors' do + expect(subject[:flags][:yaml_errors]).to be false + end + + it 'does not contain field that normally holds an error' do + expect(subject).not_to have_key(:yaml_errors) + end + end + end +end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3a32cb394dd08026fee2fde23ddff1003b4d170a --- /dev/null +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe PipelineSerializer do + let(:user) { create(:user) } + + let(:serializer) do + described_class.new(user: user) + end + + let(:entity) do + serializer.represent(resource) + end + + subject { entity.as_json } + + describe '#represent' do + context 'when used without pagination' do + it 'created a not paginated serializer' do + expect(serializer).not_to be_paginated + end + + context 'when a single object is being serialized' do + let(:resource) { create(:ci_empty_pipeline) } + + it 'serializers the pipeline object' do + expect(subject[:id]).to eq resource.id + end + end + + context 'when multiple objects are being serialized' do + let(:resource) { create_list(:ci_pipeline, 2) } + + it 'serializers the array of pipelines' do + expect(subject).not_to be_empty + end + end + end + + context 'when used with pagination' do + let(:request) { spy('request') } + let(:response) { spy('response') } + let(:pagination) { {} } + + before do + allow(request) + .to receive(:query_parameters) + .and_return(pagination) + end + + let(:serializer) do + described_class.new(user: user) + .with_pagination(request, response) + end + + it 'created a paginated serializer' do + expect(serializer).to be_paginated + end + + context 'when resource does is not paginatable' do + context 'when a single pipeline object is being serialized' do + let(:resource) { create(:ci_empty_pipeline) } + let(:pagination) { { page: 1, per_page: 1 } } + + it 'raises error' do + expect { subject } + .to raise_error(PipelineSerializer::InvalidResourceError) + end + end + end + + context 'when resource is paginatable relation' do + let(:resource) { Ci::Pipeline.all } + let(:pagination) { { page: 1, per_page: 2 } } + + context 'when a single pipeline object is present in relation' do + before { create(:ci_empty_pipeline) } + + it 'serializes pipeline relation' do + expect(subject.first).to have_key :id + end + end + + context 'when a multiple pipeline objects are being serialized' do + before { create_list(:ci_empty_pipeline, 3) } + + it 'serializes appropriate number of objects' do + expect(subject.count).to be 2 + end + + it 'appends relevant headers' do + expect(response).to receive(:[]=).with('X-Total', '3') + expect(response).to receive(:[]=).with('X-Total-Pages', '2') + expect(response).to receive(:[]=).with('X-Per-Page', '2') + + subject + end + end + end + end + end +end diff --git a/spec/serializers/request_aware_entity_spec.rb b/spec/serializers/request_aware_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..aa666b961dc26b82620554e46eeb5e44fa675ea5 --- /dev/null +++ b/spec/serializers/request_aware_entity_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe RequestAwareEntity do + subject do + Class.new.include(described_class).new + end + + it 'includes URL helpers' do + expect(subject).to respond_to(:namespace_project_path) + end + + it 'includes method for checking abilities' do + expect(subject).to respond_to(:can?) + end + + it 'fetches request from options' do + expect(subject).to receive(:options) + .and_return({ request: 'some value' }) + + expect(subject.request).to eq 'some value' + end +end diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ab40d08432a79e098ade6de1dc10964c26dfae3 --- /dev/null +++ b/spec/serializers/stage_entity_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe StageEntity do + let(:pipeline) { create(:ci_pipeline) } + let(:request) { double('request') } + let(:user) { create(:user) } + + let(:entity) do + described_class.new(stage, request: request) + end + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end + + before do + allow(request).to receive(:user).and_return(user) + create(:ci_build, :success, pipeline: pipeline) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains relevant fields' do + expect(subject).to include :name, :status, :path + end + + it 'contains detailed status' do + expect(subject[:status]).to include :text, :label, :group, :icon + expect(subject[:status][:label]).to eq 'passed' + end + + it 'contains valid name' do + expect(subject[:name]).to eq 'test' + end + + it 'contains path to the stage' do + expect(subject[:path]) + .to include "pipelines/#{pipeline.id}##{stage.name}" + end + + it 'contains path to the stage dropdown' do + expect(subject[:dropdown_path]) + .to include "pipelines/#{pipeline.id}/stage.json?stage=test" + end + + it 'contains stage title' do + expect(subject[:title]).to eq 'test: passed' + end + end +end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..89428b4216ecdb6b0b2f9e33e850837fd9a9d41a --- /dev/null +++ b/spec/serializers/status_entity_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe StatusEntity do + let(:entity) { described_class.new(status) } + + let(:status) do + Gitlab::Ci::Status::Success.new(double('object'), double('user')) + end + + before do + allow(status).to receive(:has_details?).and_return(true) + allow(status).to receive(:details_path).and_return('some/path') + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains status details' do + expect(subject).to include :text, :icon, :label, :group + expect(subject).to include :has_details, :details_path + end + end +end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..87f093ee8ce9183a0e0434017bda56fdbe1d6559 --- /dev/null +++ b/spec/services/access_token_validation_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe AccessTokenValidationService, services: true do + describe ".include_any_scope?" do + it "returns true if the required scope is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:api])).to be(true) + end + + it "returns true if more than one of the required scopes is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes is an exact match for the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + end + + it 'returns true if the list of required scopes is blank' do + token = double("token", scopes: []) + + expect(described_class.new(token).include_any_scope?([])).to be(true) + end + + it "returns false if there are no scopes in common between the required scopes and the token scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4aadd009f3ecf0316e6ae28b9de734f317a29e69..ceaca96e25b377a1ace0e89bef1de526261ea9d1 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do expect(result.manual_actions).not_to be_empty end end + + context 'with environment' do + before do + config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates the environment' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).not_to be_nil + end + end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index e7624e707254451b69d3b15353129e0113414687..2a0f00ce93721f49e203d9a5890e7f3e81929071 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -583,7 +583,7 @@ describe GitPushService, services: true do service.push_commits = [commit] expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, %i(readme)) + with(project.id, %i(readme), %i(commit_count repository_size)) service.update_caches end @@ -596,7 +596,7 @@ describe GitPushService, services: true do it 'does not flush any conditional caches' do expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, []). + with(project.id, [], %i(commit_count repository_size)). and_call_original service.update_caches @@ -604,6 +604,25 @@ describe GitPushService, services: true do end end + describe '#process_commit_messages' do + let(:service) do + described_class.new(project, + user, + oldrev: sample_commit.parent_id, + newrev: sample_commit.id, + ref: 'refs/heads/master') + end + + it 'only schedules a limited number of commits' do + allow(service).to receive(:push_commits). + and_return(Array.new(1000, double(:commit, to_hash: {}))) + + expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times + + service.process_commit_messages + end + end + def execute_service(project, user, oldrev, newrev, ref) service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) service.execute diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 71a0b8e2a12b03c580f84a293e2eb01e2acef5a7..14717a7455d3de3d56ea5709b00ec163b55b5e65 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' -describe Groups::CreateService, services: true do - let!(:user) { create(:user) } +describe Groups::CreateService, '#execute', services: true do + let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } - describe "execute" do - let!(:service) { described_class.new(user, group_params ) } + describe 'visibility level restrictions' do + let!(:service) { described_class.new(user, group_params) } + subject { service.execute } context "create groups without restricted visibility level" do @@ -14,7 +15,29 @@ describe Groups::CreateService, services: true do context "cannot create group with restricted visibility level" do before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } + it { is_expected.not_to be_persisted } end end + + describe 'creating subgroup' do + let!(:group) { create(:group) } + let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } + + subject { service.execute } + + context 'as group owner' do + before { group.add_owner(user) } + + it { is_expected.to be_persisted } + end + + context 'as guest' do + it 'does not save group and returns an error' do + is_expected.not_to be_persisted + expect(subject.errors[:parent_id].first).to eq('manage access required to create subgroup') + expect(subject.parent_id).to be_nil + end + end + end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 9c2331144a04d09d674d6b7f6c19ec7d53f4fa75..531180e48a1db0444550c63b757e9393f07c9f19 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Groups::UpdateService, services: true do - let!(:user) { create(:user) } - let!(:private_group) { create(:group, :private) } - let!(:internal_group) { create(:group, :internal) } - let!(:public_group) { create(:group, :public) } + let!(:user) { create(:user) } + let!(:private_group) { create(:group, :private) } + let!(:internal_group) { create(:group, :internal) } + let!(:public_group) { create(:group, :public) } describe "#execute" do context "project visibility_level validation" do context "public group with public projects" do - let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) } + let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do public_group.add_user(user, Gitlab::Access::MASTER) @@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do end context "internal group with internal project" do - let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do internal_group.add_user(user, Gitlab::Access::MASTER) @@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do end context "unauthorized visibility_level validation" do - let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } before do internal_group.add_user(user, Gitlab::Access::MASTER) end @@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do expect(internal_group.errors.count).to eq(1) end end + + context 'rename group' do + let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + + before do + internal_group.add_user(user, Gitlab::Access::MASTER) + create(:project, :internal, group: internal_group) + end + + it 'returns true' do + expect(service.execute).to eq(true) + end + + context 'error moving group' do + before do + allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + end + + it 'does not raise an error' do + expect { service.execute }.not_to raise_error + end + + it 'returns false' do + expect(service.execute).to eq(false) + end + + it 'has the right error' do + service.execute + + expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError') + end + + it "hasn't changed the path" do + expect { service.execute}.not_to change { internal_group.reload.path} + end + end + end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 5f3020b6525475c6248c97e892bca123cdb5fed5..0475f38fe5effa2c4150cff46ff43db7f81bed21 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -52,7 +52,10 @@ describe Issuable::BulkUpdateService, services: true do context 'when the new assignee ID is a valid user' do it 'succeeds' do - result = bulk_update(issue, assignee_id: create(:user).id) + new_assignee = create(:user) + project.team << [new_assignee, :developer] + + result = bulk_update(issue, assignee_id: new_assignee.id) expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) @@ -60,15 +63,16 @@ describe Issuable::BulkUpdateService, services: true do it 'updates the assignee to the use ID passed' do assignee = create(:user) + project.team << [assignee, :developer] expect { bulk_update(issue, assignee_id: assignee.id) } .to change { issue.reload.assignee }.from(user).to(assignee) end end - context 'when the new assignee ID is -1' do - it 'unassigns the issues' do - expect { bulk_update(issue, assignee_id: -1) } + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it "unassigns the issues" do + expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) } .to change { issue.reload.assignee }.to(nil) end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 8bde61ee336422a5f19db67b1675351ec4af97aa..ac3834c32ff69d1969637e029e510d34ebb4ee24 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -135,6 +135,8 @@ describe Issues::CreateService, services: true do end end + it_behaves_like 'issuable create service' + it_behaves_like 'new issuable record that supports slash commands' context 'for a merge request' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 500d224ff98434fa39671e17b419ab13aec52e1c..d83b09fd32ca484a3e5232fe3b4e98888848a87a 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -142,6 +142,17 @@ describe Issues::UpdateService, services: true do update_issue(confidential: true) end + + it 'does not update assignee_id with unauthorized users' do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + update_issue(confidential: true) + non_member = create(:user) + original_assignee = issue.assignee + + update_issue(assignee_id: non_member.id) + + expect(issue.reload.assignee_id).to eq(original_assignee.id) + end end context 'todos' do @@ -376,5 +387,10 @@ describe Issues::UpdateService, services: true do let(:mentionable) { issue } include_examples 'updating mentions', Issues::UpdateService end + + include_examples 'issuable update service' do + let(:open_issuable) { issue } + let(:closed_issuable) { create(:closed_issue, project: project) } + end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index b81428890756d6d902c90fa12251b6a1bc63a6a1..673c0bd6c9c8e774e5b0cdfcc62a10ac1201f6ef 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -84,6 +84,8 @@ describe MergeRequests::CreateService, services: true do end end + it_behaves_like 'issuable create service' + context 'while saving references to issues that the created merge request closes' do let(:first_issue) { create(:issue, project: project) } let(:second_issue) { create(:issue, project: project) } diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 05cdbe5287a4dbf4b7349a277e7036de8ea31551..35804d41b4605446de979e4e91459df3239493f0 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -11,6 +11,7 @@ describe MergeRequests::MergeRequestDiffCacheService do expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true)) + allow_any_instance_of(Repository).to receive(:diffable?).and_return(true) subject.execute(merge_request) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 790ef765f3a1b641faacbf46c9a431a19cf1b78a..7d73c0ea5d0e7dd70ec748ba5b99de9adbc2bc4f 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -121,6 +121,99 @@ describe MergeRequests::UpdateService, services: true do end end + context 'merge' do + let(:opts) do + { + merge: merge_request.diff_head_sha + } + end + + let(:service) { MergeRequests::UpdateService.new(project, user, opts) } + + context 'without pipeline' do + before do + merge_request.merge_error = 'Error' + + perform_enqueued_jobs do + service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.state).to eq('merged') } + it { expect(@merge_request.merge_error).to be_nil } + end + + context 'with finished pipeline' do + before do + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + status: :success) + + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.state).to eq('merged') } + end + + context 'with active pipeline' do + before do + service_mock = double + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + + expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user). + and_return(service_mock) + expect(service_mock).to receive(:execute).with(merge_request) + end + + it { service.execute(merge_request) } + end + + context 'with a non-authorised user' do + let(:visitor) { create(:user) } + let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) } + + before do + merge_request.update_attribute(:merge_error, 'Error') + + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request.state).to eq('opened') } + it { expect(@merge_request.merge_error).not_to be_nil } + end + + context 'MR can not be merged when note sha != MR sha' do + let(:opts) do + { + merge: 'other_commit' + } + end + + before do + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request.state).to eq('opened') } + end + end + context 'todos' do let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } @@ -320,5 +413,10 @@ describe MergeRequests::UpdateService, services: true do expect(issue_ids).to be_empty end end + + include_examples 'issuable update service' do + let(:open_issuable) { merge_request } + let(:closed_issuable) { create(:closed_merge_request, source_project: project) } + end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 25804696d2e9ca965f749fefa3354ff20b23fea8..b0cc3ce5f5a5c74b9845e7039070120f0ea5244c 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -63,6 +63,17 @@ describe Notes::CreateService, services: true do expect(note.note).to eq "HELLO\nWORLD" end end + + describe '/merge with sha option' do + let(:note_text) { %(HELLO\n/merge\nWORLD) } + let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + + it 'saves the note and exectues merge command' do + note = described_class.new(project, user, params).execute + + expect(note.note).to eq "HELLO\nWORLD" + end + end end end diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb index d1099884a02480a7f198eeed6a2b8e0fd2ce64fb..1a64c8bbf00df05a396be79b356f8b73afa3e3ef 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -5,6 +5,8 @@ describe Notes::SlashCommandsService, services: true do let(:project) { create(:empty_project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } + + before { project.team << [assignee, :master] } end shared_examples 'note on noteable that does not support slash commands' do @@ -84,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do expect(note.noteable).to be_open end end + + describe '/spend' do + let(:note_text) { '/spend 1h' } + + it 'updates the spent time on the noteable' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq '' + expect(note.noteable.time_spent).to eq(3600) + end + end end describe 'note with command & text' do diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 64d15c0523c4fc72ac8e27f29191a4b9a3b5cb03..8e6142111163080d8a7254a843dfe7ff7d5479b4 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -5,10 +5,12 @@ describe Projects::ForkService, services: true do before do @from_namespace = create(:namespace) @from_user = create(:user, namespace: @from_namespace ) + avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project = create(:project, creator_id: @from_user.id, namespace: @from_namespace, star_count: 107, + avatar: avatar, description: 'wow such project') @to_namespace = create(:namespace) @to_user = create(:user, namespace: @to_namespace) @@ -36,6 +38,17 @@ describe Projects::ForkService, services: true do it { expect(to_project.namespace).to eq(@to_user.namespace) } it { expect(to_project.star_count).to be_zero } it { expect(to_project.description).to eq(@from_project.description) } + it { expect(to_project.avatar.file).to be_exists } + + # This test is here because we had a bug where the from-project lost its + # avatar after being forked. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 + it "after forking the from-project still has its avatar" do + # If we do not fork the project first we cannot detect the bug. + expect(to_project).to be_persisted + + expect(@from_project.avatar.file).to be_exists + end end end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..063b3bd76eb4e5d85cf7e5a519da1d5fe200a383 --- /dev/null +++ b/spec/services/projects/participants_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::ParticipantsService, services: true do + describe '#groups' do + describe 'avatar_url' do + let(:project) { create(:empty_project, :public) } + let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) } + let(:user) { create(:user) } + let(:base_url) { Settings.send(:build_base_gitlab_url) } + let!(:group_member) { create(:group_member, group: group, user: user) } + + it 'should return an url for the avatar' do + participants = described_class.new(project, user) + groups = participants.groups + + expect(groups.size).to eq 1 + expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png" + end + + it 'should return an url for the avatar with relative url' do + stub_config_setting(relative_url_root: '/gitlab') + stub_config_setting(url: Settings.send(:build_gitlab_url)) + + participants = described_class.new(project, user) + groups = participants.groups + + expect(groups.size).to eq 1 + expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png" + end + end + end +end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index e139be19140cd6bdbefca2e1d0799e29fcd818f6..caa2396251954ee46639376b4090e60cf7c09974 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,145 +1,101 @@ require 'spec_helper' describe Projects::UpdateService, services: true do - describe :update_by_user do - before do - @user = create :user - @admin = create :user, admin: true - @project = create :project, creator_id: @user.id, namespace: @user.namespace - @opts = {} - end + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - context 'is private when updated to private' do - before do - @created_private = @project.private? + describe 'update_by_user' do + context 'when visibility_level is INTERNAL' do + it 'updates the project to internal' do + result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - update_project(@project, @user, @opts) + expect(result).to eq({ status: :success }) + expect(project).to be_internal end - - it { expect(@created_private).to be_truthy } - it { expect(@project.private?).to be_truthy } end - context 'is internal when updated to internal' do - before do - @created_private = @project.private? - - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - update_project(@project, @user, @opts) + context 'when visibility_level is PUBLIC' do + it 'updates the project to public' do + result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + expect(result).to eq({ status: :success }) + expect(project).to be_public end - - it { expect(@created_private).to be_truthy } - it { expect(@project.internal?).to be_truthy } end - context 'is public when updated to public' do + context 'when visibility levels are restricted to PUBLIC only' do before do - @created_private = @project.private? - - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - update_project(@project, @user, @opts) - end - - it { expect(@created_private).to be_truthy } - it { expect(@project.public?).to be_truthy } - end - - context 'respect configured visibility restrictions setting' do - before(:each) do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end - context 'is private when updated to private' do - before do - @created_private = @project.private? - - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - update_project(@project, @user, @opts) + context 'when visibility_level is INTERNAL' do + it 'updates the project to internal' do + result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) + expect(result).to eq({ status: :success }) + expect(project).to be_internal end - - it { expect(@created_private).to be_truthy } - it { expect(@project.private?).to be_truthy } end - context 'is internal when updated to internal' do - before do - @created_private = @project.private? + context 'when visibility_level is PUBLIC' do + it 'does not update the project to public' do + result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - update_project(@project, @user, @opts) + expect(result).to eq({ status: :error, message: 'Visibility level unallowed' }) + expect(project).to be_private end - it { expect(@created_private).to be_truthy } - it { expect(@project.internal?).to be_truthy } - end - - context 'is private when updated to public' do - before do - @created_private = @project.private? - - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - update_project(@project, @user, @opts) + context 'when updated by an admin' do + it 'updates the project to public' do + result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + expect(result).to eq({ status: :success }) + expect(project).to be_public + end end - - it { expect(@created_private).to be_truthy } - it { expect(@project.private?).to be_truthy } - end - - context 'is public when updated to public by admin' do - before do - @created_private = @project.private? - - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - update_project(@project, @admin, @opts) - end - - it { expect(@created_private).to be_truthy } - it { expect(@project.public?).to be_truthy } end end end - describe :visibility_level do - let(:user) { create :user, admin: true } + describe 'visibility_level' do let(:project) { create(:project, :internal) } let(:forked_project) { create(:forked_project_with_submodules, :internal) } - let(:opts) { {} } before do forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id) forked_project.save - - @created_internal = project.internal? - @fork_created_internal = forked_project.internal? end - context 'updates forks visibility level when parent set to more restrictive' do - before do - opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - update_project(project, user, opts).inspect - end + it 'updates forks visibility level when parent set to more restrictive' do + opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } + + expect(project).to be_internal + expect(forked_project).to be_internal - it { expect(@created_internal).to be_truthy } - it { expect(@fork_created_internal).to be_truthy } - it { expect(project.private?).to be_truthy } - it { expect(project.forks.first.private?).to be_truthy } + expect(update_project(project, admin, opts)).to eq({ status: :success }) + + expect(project).to be_private + expect(forked_project.reload).to be_private end - context 'does not update forks visibility level when parent set to less restrictive' do - before do - opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - update_project(project, user, opts).inspect - end + it 'does not update forks visibility level when parent set to less restrictive' do + opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + + expect(project).to be_internal + expect(forked_project).to be_internal - it { expect(@created_internal).to be_truthy } - it { expect(@fork_created_internal).to be_truthy } - it { expect(project.public?).to be_truthy } - it { expect(project.forks.first.internal?).to be_truthy } + expect(update_project(project, admin, opts)).to eq({ status: :success }) + + expect(project).to be_public + expect(forked_project.reload).to be_internal end end + it 'returns an error result when record cannot be updated' do + result = update_project(project, admin, { name: 'foo&bar' }) + + expect(result).to eq({ status: :error, message: 'Project could not be updated' }) + end + def update_project(project, user, opts) - Projects::UpdateService.new(project, user, opts).execute + described_class.new(project, user, opts).execute end end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index becf627a4f582cf20cd0c735fb48246025a9dd67..66fc8fc360b523e86d0e50803ac78911a9578080 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' describe SlashCommands::InterpretService, services: true do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:developer) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:bug) { create(:label, project: project, title: 'Bug') } + let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } before do project.team << [developer, :developer] @@ -210,6 +211,46 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'estimate command' do + it 'populates time_estimate: 3600 if content contains /estimate 1h' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(time_estimate: 3600) + end + end + + shared_examples 'spend command' do + it 'populates spend_time: 3600 if content contains /spend 1h' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { duration: 3600, user: developer }) + end + end + + shared_examples 'spend command with negative time' do + it 'populates spend_time: -1800 if content contains /spend -30m' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { duration: -1800, user: developer }) + end + end + + shared_examples 'remove_estimate command' do + it 'populates time_estimate: 0 if content contains /remove_estimate' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(time_estimate: 0) + end + end + + shared_examples 'remove_time_spent command' do + it 'populates spend_time: :reset if content contains /remove_time_spent' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { duration: :reset, user: developer }) + end + end + shared_examples 'empty command' do it 'populates {} if content contains an unsupported command' do _, updates = service.execute(content, issuable) @@ -218,6 +259,14 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'merge command' do + it 'runs merge command if content contains /merge' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(merge: merge_request.diff_head_sha) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -238,6 +287,64 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { merge_request } end + context 'merge command' do + let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) } + + it_behaves_like 'merge command' do + let(:content) { '/merge' } + let(:issuable) { merge_request } + end + + context 'can not be merged when logged user does not have permissions' do + let(:service) { described_class.new(project, create(:user)) } + + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { merge_request } + end + end + + context 'can not be merged when sha does not match' do + let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) } + + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { merge_request } + end + end + + context 'when sha is missing' do + let(:service) { described_class.new(project, developer, {}) } + + it 'precheck passes and returns merge command' do + _, updates = service.execute('/merge', merge_request) + + expect(updates).to eq(merge: nil) + end + end + + context 'issue can not be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { issue } + end + end + + context 'non persisted merge request cant be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { build(:merge_request) } + end + end + + context 'not persisted merge request can not be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { build(:merge_request, source_project: project) } + end + end + end + it_behaves_like 'title command' do let(:content) { '/title A brand new title' } let(:issuable) { issue } @@ -451,6 +558,51 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { merge_request } end + it_behaves_like 'estimate command' do + let(:content) { '/estimate 1h' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/estimate' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/estimate abc' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command' do + let(:content) { '/spend 1h' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with negative time' do + let(:content) { '/spend -30m' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/spend' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/spend abc' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_estimate command' do + let(:content) { '/remove_estimate' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_time_spent command' do + let(:content) { '/remove_time_spent' } + let(:issuable) { issue } + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 0e8adb687212bd1149c986788b0e079c584dc4ad..4042f2b05123d3777d81a4075f6defac0f8c1df7 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -740,4 +740,69 @@ describe SystemNoteService, services: true do expect(note.note).to include(issue.to_reference) end end + + describe '.change_time_estimate' do + subject { described_class.change_time_estimate(noteable, project, author) } + + it_behaves_like 'a system note' + + context 'with a time estimate' do + it 'sets the note text' do + noteable.update_attribute(:time_estimate, 277200) + + expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h" + end + end + + context 'without a time estimate' do + it 'sets the note text' do + expect(subject.note).to eq "Removed time estimate on this issue" + end + end + end + + describe '.change_time_spent' do + # We need a custom noteable in order to the shared examples to be green. + let(:noteable) do + mr = create(:merge_request, source_project: project) + mr.spend_time(duration: 360000, user: author) + mr.save! + mr + end + + subject do + described_class.change_time_spent(noteable, project, author) + end + + it_behaves_like 'a system note' + + context 'when time was added' do + it 'sets the note text' do + spend_time!(277200) + + expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request" + end + end + + context 'when time was subtracted' do + it 'sets the note text' do + spend_time!(-277200) + + expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request" + end + end + + context 'when time was removed' do + it 'sets the note text' do + spend_time!(:reset) + + expect(subject.note).to eq "Removed time spent on this merge request" + end + end + + def spend_time!(seconds) + noteable.spend_time(duration: seconds, user: author) + noteable.save! + end + end end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..690fe9794928ac5f1f90b6bb1370f85022a17a10 --- /dev/null +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -0,0 +1,211 @@ +require 'spec_helper' + +describe Users::RefreshAuthorizedProjectsService do + let(:project) { create(:empty_project) } + let(:user) { project.namespace.owner } + let(:service) { described_class.new(user) } + + def create_authorization(project, user, access_level = Gitlab::Access::MASTER) + ProjectAuthorization. + create!(project: project, user: user, access_level: access_level) + end + + describe '#execute', :redis do + it 'refreshes the authorizations using a lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). + and_return('foo') + + expect(Gitlab::ExclusiveLease).to receive(:cancel). + with(an_instance_of(String), 'foo') + + expect(service).to receive(:execute_without_lease) + + service.execute + end + end + + describe '#execute_without_lease' do + before do + user.project_authorizations.delete_all + end + + it 'updates the authorized projects of the user' do + project2 = create(:empty_project) + to_remove = create_authorization(project2, user) + + expect(service).to receive(:update_authorizations). + with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute_without_lease + end + + it 'sets the access level of a project to the highest available level' do + to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) + + expect(service).to receive(:update_authorizations). + with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute_without_lease + end + + it 'returns a User' do + expect(service.execute_without_lease).to be_an_instance_of(User) + end + end + + describe '#update_authorizations' do + context 'when there are no rows to add and remove' do + it 'does not change authorizations' do + expect(user).not_to receive(:remove_project_authorizations) + expect(ProjectAuthorization).not_to receive(:insert_authorizations) + + service.update_authorizations([], []) + end + + context 'when the authorized projects column is not set' do + before do + user.update!(authorized_projects_populated: nil) + end + + it 'populates the authorized projects column' do + service.update_authorizations([], []) + + expect(user.authorized_projects_populated).to eq true + end + end + + context 'when the authorized projects column is set' do + before do + user.update!(authorized_projects_populated: true) + end + + it 'does nothing' do + expect(user).not_to receive(:set_authorized_projects_column) + + service.update_authorizations([], []) + end + end + end + + it 'removes authorizations that should be removed' do + authorization = create_authorization(project, user) + + service.update_authorizations([authorization.project_id]) + + expect(user.project_authorizations).to be_empty + end + + it 'inserts authorizations that should be added' do + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + authorizations = user.project_authorizations + + expect(authorizations.length).to eq(1) + expect(authorizations[0].user_id).to eq(user.id) + expect(authorizations[0].project_id).to eq(project.id) + expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER) + end + + it 'populates the authorized projects column' do + # make sure we start with a nil value no matter what the default in the + # factory may be. + user.update!(authorized_projects_populated: nil) + + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + expect(user.authorized_projects_populated).to eq(true) + end + end + + describe '#fresh_access_levels_per_project' do + let(:hash) { service.fresh_access_levels_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the access levels' do + expect(hash.values).to eq([Gitlab::Access::MASTER]) + end + end + + describe '#current_authorizations_per_project' do + before { create_authorization(project, user) } + + let(:hash) { service.current_authorizations_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the project authorization rows' do + expect(hash.values.length).to eq(1) + + value = hash.values[0] + + expect(value.project_id).to eq(project.id) + expect(value.access_level).to eq(Gitlab::Access::MASTER) + end + end + + describe '#current_authorizations' do + context 'without authorizations' do + it 'returns an empty list' do + expect(service.current_authorizations.empty?).to eq(true) + end + end + + context 'with an authorization' do + before { create_authorization(project, user) } + + let(:row) { service.current_authorizations.take } + + it 'returns the currently authorized projects' do + expect(service.current_authorizations.length).to eq(1) + end + + it 'includes the project ID for every row' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level for every row' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end + + describe '#fresh_authorizations' do + it 'returns the new authorized projects' do + expect(service.fresh_authorizations.length).to eq(1) + end + + it 'returns the highest access level' do + project.team.add_guest(user) + + rows = service.fresh_authorizations.to_a + + expect(rows.length).to eq(1) + expect(rows.first.access_level).to eq(Gitlab::Access::MASTER) + end + + context 'every returned row' do + let(:row) { service.fresh_authorizations.take } + + it 'includes the project ID' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end +end diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb new file mode 100644 index 0000000000000000000000000000000000000000..ea38fe4f5b8e918b8e5ecaed37373aa818459288 --- /dev/null +++ b/spec/support/api/repositories_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'disabled repository' do + before do + project.project_feature.update_attributes!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + expect(project.feature_available?(:repository, current_user)).to be false + end +end diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/api/status_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..3481749a7f0e6f1f0dcff2c1bfea5c8991ace8d4 --- /dev/null +++ b/spec/support/api/status_shared_examples.rb @@ -0,0 +1,42 @@ +# Specs for status checking. +# +# Requires an API request: +# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } +shared_examples_for '400 response' do + before do + # Fires the request + request + end + + it 'returns 400' do + expect(response).to have_http_status(400) + end +end + +shared_examples_for '403 response' do + before do + # Fires the request + request + end + + it 'returns 403' do + expect(response).to have_http_status(403) + end +end + +shared_examples_for '404 response' do + let(:message) { nil } + before do + # Fires the request + request + end + + it 'returns 404' do + expect(response).to have_http_status(404) + expect(json_response).to be_an Object + + if message.present? + expect(json_response['message']).to eq(message) + end + end +end diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..210cd5817e068170729a24580be40195bd0b5754 --- /dev/null +++ b/spec/support/api/time_tracking_shared_examples.rb @@ -0,0 +1,132 @@ +shared_examples 'an unauthorized API user' do + it { is_expected.to eq(403) } +end + +shared_examples 'time tracking endpoints' do |issuable_name| + issuable_collection_name = issuable_name.pluralize + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') } + + it_behaves_like 'an unauthorized API user' + end + + it "sets the time estimate for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + + expect(response).to have_http_status(200) + expect(json_response['human_time_estimate']).to eq('1w') + end + + describe 'updating the current estimate' do + before do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + end + + context 'when duration has a bad format' do + it 'does not modify the original estimate' do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo' + + expect(response).to have_http_status(400) + expect(issuable.reload.human_time_estimate).to eq('1w') + end + end + + context 'with a valid duration' do + it 'updates the estimate' do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h' + + expect(response).to have_http_status(200) + expect(issuable.reload.human_time_estimate).to eq('3w 1h') + end + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets the time estimate for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user) + + expect(response).to have_http_status(200) + expect(json_response['time_estimate']).to eq(0) + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do + context 'with an unauthorized user' do + subject do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member), + duration: '2h' + end + + it_behaves_like 'an unauthorized API user' + end + + it "add spent time for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '2h' + + expect(response).to have_http_status(201) + expect(json_response['human_total_time_spent']).to eq('2h') + end + + context 'when subtracting time' do + it 'subtracts time of the total spent time' do + issuable.update_attributes!(spend_time: { duration: 7200, user: user }) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '-1h' + + expect(response).to have_http_status(201) + expect(json_response['total_time_spent']).to eq(3600) + end + end + + context 'when time to subtract is greater than the total spent time' do + it 'does not modify the total time spent' do + issuable.update_attributes!(spend_time: { duration: 7200, user: user }) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '-1w' + + expect(response).to have_http_status(400) + expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets spent time for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user) + + expect(response).to have_http_status(200) + expect(json_response['total_time_spent']).to eq(0) + end + end + + describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do + it "returns the time stats for #{issuable_name}" do + issuable.update_attributes!(spend_time: { duration: 1800, user: user }, + time_estimate: 3600) + + get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user) + + expect(response).to have_http_status(200) + expect(json_response['total_time_spent']).to eq(1800) + expect(json_response['time_estimate']).to eq(3600) + end + end +end diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/chat_slash_commands_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..4dfa29849ee7c4fc0ff9a224a3be904a2f75b25a --- /dev/null +++ b/spec/support/chat_slash_commands_shared_examples.rb @@ -0,0 +1,97 @@ +RSpec.shared_examples 'chat slash commands service' do + describe "Associations" do + it { is_expected.to respond_to :token } + it { is_expected.to have_many :chat_names } + end + + describe '#valid_token?' do + subject { described_class.new } + + context 'when the token is empty' do + it 'is false' do + expect(subject.valid_token?('wer')).to be_falsey + end + end + + context 'when there is a token' do + before do + subject.token = '123' + end + + it 'accepts equal tokens' do + expect(subject.valid_token?('123')).to be_truthy + end + end + end + + describe '#trigger' do + subject { described_class.new } + + context 'no token is passed' do + let(:params) { Hash.new } + + it 'returns nil' do + expect(subject.trigger(params)).to be_nil + end + end + + context 'with a token passed' do + let(:project) { create(:empty_project) } + let(:params) { { token: 'token' } } + + before do + allow(subject).to receive(:token).and_return('token') + end + + context 'no user can be found' do + context 'when no url can be generated' do + it 'responds with the authorize url' do + response = subject.trigger(params) + + expect(response[:response_type]).to eq :ephemeral + expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" + end + end + + context 'when an auth url can be generated' do + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + + let(:service) do + project.create_mattermost_slash_commands_service( + properties: { token: 'token' } + ) + end + + it 'generates the url' do + response = service.trigger(params) + + expect(response[:text]).to start_with(':wave: Hi there!') + end + end + end + + context 'when the user is authenticated' do + let!(:chat_name) { create(:chat_name, service: subject) } + let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } + + subject do + described_class.create(project: project, properties: { token: 'token' }) + end + + it 'triggers the command' do + expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + + subject.trigger(params) + end + end + end + end +end diff --git a/spec/support/controllers/githubish_import_controller_shared_context.rb b/spec/support/controllers/githubish_import_controller_shared_context.rb new file mode 100644 index 0000000000000000000000000000000000000000..e71994edec68dec3197723f283586d52f3e7352f --- /dev/null +++ b/spec/support/controllers/githubish_import_controller_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'a GitHub-ish import controller' do + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:access_params) { { github_access_token: token } } + + before do + sign_in(user) + allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true) + end +end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..d0fd2d52004f62e8185e3701b6435ea94162a75f --- /dev/null +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -0,0 +1,232 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +def assign_session_token(provider) + session[:"#{provider}_access_token"] = 'asdasd12345' +end + +shared_examples 'a GitHub-ish import controller: POST personal_access_token' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "updates access token" do + token = 'asdfasdf9876' + + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:user).and_return(true) + + post :personal_access_token, personal_access_token: token + + expect(session[:"#{provider}_access_token"]).to eq(token) + expect(controller).to redirect_to(status_import_url) + end +end + +shared_examples 'a GitHub-ish import controller: GET new' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "redirects to status if we already have a token" do + assign_session_token(provider) + allow(controller).to receive(:logged_in_with_provider?).and_return(false) + + get :new + + expect(controller).to redirect_to(status_import_url) + end + + it "renders the :new page if no token is present in session" do + get :new + + expect(response).to render_template(:new) + end +end + +shared_examples 'a GitHub-ish import controller: GET status' do + let(:new_import_url) { public_send("new_import_#{provider}_url") } + let(:user) { create(:user) } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:org) { OpenStruct.new(login: 'company') } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } + let(:extra_assign_expectations) { {} } + + before do + assign_session_token(provider) + end + + it "assigns variables" do + project = create(:empty_project, import_type: provider, creator_id: user.id) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([repo, org_repo]) + extra_assign_expectations.each do |key, value| + expect(assigns(key)).to eq(value) + end + end + + it "does not show already added project" do + project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') + stub_client(repos: [repo], orgs: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([]) + end + + it "handles an invalid access token" do + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:repos).and_raise(Octokit::Unauthorized) + + get :status + + expect(session[:"#{provider}_access_token"]).to be_nil + expect(controller).to redirect_to(new_import_url) + expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") + end +end + +shared_examples 'a GitHub-ish import controller: POST create' do + let(:user) { create(:user) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + stub_client(user: provider_user, repo: provider_repo) + assign_session_token(provider) + end + + context "when the repository owner is the provider user" do + context "when the provider user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the provider user and GitLab user's usernames don't match" do + let(:provider_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the provider user" do + let(:other_username) { "someone_else" } + + before do + provider_repo.owner = OpenStruct.new(login: other_username) + assign_session_token(provider) + end + + context "when a namespace with the provider user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "creates a project using user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when a namespace with the provider user's username doesn't exist" do + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, target_namespace: provider_repo.name, format: :js + end + end + + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end + + it "doesn't create the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context 'user has chosen a namespace and name for the project' do + let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } + let(:test_name) { 'test_name' } + + it 'takes the selected namespace and name' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } + end + + it 'takes the selected name and default namespace' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { new_name: test_name, format: :js } + end + end + end +end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 8e19a6c92e2e29c7d5b6b1adbe39760da44ddc2e..35b40d731913c87ca3ba3f7971795630a0dd275e 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -2,7 +2,6 @@ # Note: The ABC size is large here because we have a method generating test cases with # multiple nested contexts. This shouldn't count as a violation. - module CycleAnalyticsHelpers module TestGeneration # Generate the most common set of specs that all cycle analytics phases need to have. @@ -51,7 +50,7 @@ module CycleAnalyticsHelpers end median_time_difference = time_differences.sort[2] - expect(subject.send(phase)).to be_within(5).of(median_time_difference) + expect(subject[phase].median).to be_within(5).of(median_time_difference) end context "when the data belongs to another project" do @@ -83,7 +82,7 @@ module CycleAnalyticsHelpers # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end @@ -106,7 +105,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -126,7 +125,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -145,7 +144,7 @@ module CycleAnalyticsHelpers post_fn[self, data] if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -153,7 +152,7 @@ module CycleAnalyticsHelpers context "when none of the start / end conditions are matched" do it "returns nil" do - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb index 8c407b867fef5683ed5df7945a3880772307b3c2..a7605cd483aced0fb8209f54ba37df15065d8cc6 100644 --- a/spec/support/fake_u2f_device.rb +++ b/spec/support/fake_u2f_device.rb @@ -5,7 +5,7 @@ class FakeU2fDevice @page = page @name = name end - + def respond_to_u2f_registration app_id = @page.evaluate_script('gon.u2f.app_id') challenges = @page.evaluate_script('gon.u2f.challenges') @@ -28,6 +28,7 @@ class FakeU2fDevice u2f.sign = function(appId, challenges, signRequests, callback) { callback(#{json_response}); }; + window.gl.u2fAuthenticate.start(); ") end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 194620d0a68bab42795c5d43ecb2b3ff889b6ee3..a4713e53f63bd5747d550e0193601b12c3753bd9 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -76,7 +76,7 @@ shared_examples 'issuable record that supports slash commands in its description expect(page).not_to have_content '/assign @bob' expect(page).not_to have_content '/label ~bug' expect(page).not_to have_content '/milestone %"ASAP"' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' issuable.reload @@ -97,7 +97,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/close") expect(page).not_to have_content '/close' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(issuable.reload).to be_closed end @@ -114,7 +114,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/close") expect(page).not_to have_content '/close' - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Commands applied' expect(issuable).to be_open end @@ -132,7 +132,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/reopen") expect(page).not_to have_content '/reopen' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(issuable.reload).to be_open end @@ -149,7 +149,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/reopen") expect(page).not_to have_content '/reopen' - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Commands applied' expect(issuable).to be_closed end @@ -162,7 +162,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/title Awesome new title") expect(page).not_to have_content '/title' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(issuable.reload.title).to eq 'Awesome new title' end @@ -179,7 +179,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/title Awesome new title") expect(page).not_to have_content '/title' - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Commands applied' expect(issuable.reload.title).not_to eq 'Awesome new title' end @@ -191,7 +191,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/todo") expect(page).not_to have_content '/todo' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' todos = TodosFinder.new(master).execute todo = todos.first @@ -222,7 +222,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/done") expect(page).not_to have_content '/done' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(todo.reload).to be_done end @@ -235,7 +235,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/subscribe") expect(page).not_to have_content '/subscribe' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(issuable.subscribed?(master, project)).to be_truthy end @@ -252,7 +252,7 @@ shared_examples 'issuable record that supports slash commands in its description write_note("/unsubscribe") expect(page).not_to have_content '/unsubscribe' - expect(page).to have_content 'Your commands have been executed!' + expect(page).to have_content 'Commands applied' expect(issuable.subscribed?(master, project)).to be_falsy end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index 99e98eebdb4d3df9636169d146d917c2e7e4710c..0b8729db0f9e8a3fab4b4a839c7ba5e442398e0b 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -20,12 +20,26 @@ module JavaScriptFixturesHelpers # Public: Store a response object as fixture file # - # response - response object to store + # response - string or response object to store # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH) # def store_frontend_fixture(response, fixture_file_name) fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH) + fixture = response.respond_to?(:body) ? parse_response(response) : response + + FileUtils.mkdir_p(File.dirname(fixture_file_name)) + File.write(fixture_file_name, fixture) + end + + private + + # Private: Prepare a response object for use as a frontend fixture + # + # response - response object to prepare + # + def parse_response(response) fixture = response.body + fixture.force_encoding("utf-8") response_mime_type = Mime::Type.lookup(response.content_type) if response_mime_type.html? @@ -34,7 +48,7 @@ module JavaScriptFixturesHelpers link_tags = doc.css('link') link_tags.remove - scripts = doc.css('script') + scripts = doc.css("script:not([type='text/template'])") scripts.remove fixture = doc.to_html @@ -44,7 +58,6 @@ module JavaScriptFixturesHelpers fixture.gsub!(%r{="/}, "=\"http://#{test_host}/") end - FileUtils.mkdir_p(File.dirname(fixture_file_name)) - File.write(fixture_file_name, fixture) + fixture end end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c4c246a68b3357be3f0c2c6aa74e1bab4d45d48 --- /dev/null +++ b/spec/support/kubernetes_helpers.rb @@ -0,0 +1,52 @@ +module KubernetesHelpers + include Gitlab::Kubernetes + + def kube_discovery_body + { "kind" => "APIResourceList", + "resources" => [ + { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, + ], + } + end + + def kube_pods_body(*pods) + { "kind" => "PodList", + "items" => [ kube_pod ], + } + end + + # This is a partial response, it will have many more elements in reality but + # these are the ones we care about at the moment + def kube_pod(app: "valid-pod-label") + { "metadata" => { + "name" => "kube-pod", + "creationTimestamp" => "2016-11-25T19:55:19Z", + "labels" => { "app" => app }, + }, + "spec" => { + "containers" => [ + { "name" => "container-0" }, + { "name" => "container-1" }, + ], + }, + "status" => { "phase" => "Running" }, + } + end + + def kube_terminals(service, pod) + pod_name = pod['metadata']['name'] + containers = pod['spec']['containers'] + + containers.map do |container| + terminal = { + selectors: { pod: pod_name, container: container['name'] }, + url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']), + subprotocols: ['channel.k8s.io'], + headers: { 'Authorization' => ["Bearer #{service.token}"] }, + created_at: DateTime.parse(pod['metadata']['creationTimestamp']) + } + terminal[:ca_pem] = service.ca_pem if service.ca_pem.present? + terminal + end + end +end diff --git a/spec/support/matchers/be_valid_commit.rb b/spec/support/matchers/be_valid_commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..3696e4d5f03730fb92efa34b9e0c5706040ac875 --- /dev/null +++ b/spec/support/matchers/be_valid_commit.rb @@ -0,0 +1,8 @@ +RSpec::Matchers.define :be_valid_commit do + match do |actual| + actual && + actual.id == SeedRepo::Commit::ID && + actual.message == SeedRepo::Commit::MESSAGE && + actual.author_name == SeedRepo::Commit::AUTHOR_FULL_NAME + end +end diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb new file mode 100644 index 0000000000000000000000000000000000000000..e40d5ebd9a8a38bceb5547a281b5b376c22e4b7e --- /dev/null +++ b/spec/support/query_recorder.rb @@ -0,0 +1,40 @@ +module ActiveRecord + class QueryRecorder + attr_reader :log + + def initialize(&block) + @log = [] + ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) + end + + def callback(name, start, finish, message_id, values) + return if %w(CACHE SCHEMA).include?(values[:name]) + @log << values[:sql] + end + + def count + @log.count + end + + def log_message + @log.join("\n\n") + end + end +end + +RSpec::Matchers.define :exceed_query_limit do |expected| + supports_block_expectations + + match do |block| + query_count(&block) > expected + end + + failure_message_when_negated do |actual| + "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}" + end + + def query_count(&block) + @recorder = ActiveRecord::QueryRecorder.new(&block) + @recorder.count + end +end diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..98eb57f8b54b97352b852ad5310502c4aa5ba193 --- /dev/null +++ b/spec/support/reactive_caching_helpers.rb @@ -0,0 +1,42 @@ +module ReactiveCachingHelpers + def reactive_cache_key(subject, *qualifiers) + ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':') + end + + def alive_reactive_cache_key(subject, *qualifiers) + reactive_cache_key(subject, *(qualifiers + ['alive'])) + end + + def stub_reactive_cache(subject = nil, data = nil, *qualifiers) + allow(ReactiveCachingWorker).to receive(:perform_async) + allow(ReactiveCachingWorker).to receive(:perform_in) + write_reactive_cache(subject, data, *qualifiers) if data + end + + def read_reactive_cache(subject, *qualifiers) + Rails.cache.read(reactive_cache_key(subject, *qualifiers)) + end + + def write_reactive_cache(subject, data, *qualifiers) + start_reactive_cache_lifetime(subject, *qualifiers) + Rails.cache.write(reactive_cache_key(subject, *qualifiers), data) + end + + def reactive_cache_alive?(subject, *qualifiers) + Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers)) + end + + def invalidate_reactive_cache(subject, *qualifiers) + Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers)) + end + + def start_reactive_cache_lifetime(subject, *qualifiers) + Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true) + end + + def expect_reactive_cache_update_queued(subject) + expect(ReactiveCachingWorker). + to receive(:perform_in). + with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id) + end +end diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..03fa0a66b9abfdf6a3779a1e89df763f52d02715 --- /dev/null +++ b/spec/support/seed_helper.rb @@ -0,0 +1,112 @@ +# This file is specific to specs in spec/lib/gitlab/git/ + +SEED_REPOSITORY_PATH = File.expand_path('../../tmp/repositories', __dir__) +TEST_REPO_PATH = File.join(SEED_REPOSITORY_PATH, 'gitlab-git-test.git') +TEST_NORMAL_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "not-bare-repo.git") +TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git") +TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git") + +module SeedHelper + GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git" + + def ensure_seeds + if File.exist?(SEED_REPOSITORY_PATH) + FileUtils.rm_r(SEED_REPOSITORY_PATH) + end + + FileUtils.mkdir_p(SEED_REPOSITORY_PATH) + + create_bare_seeds + create_normal_seeds + create_mutable_seeds + create_broken_seeds + create_git_attributes + create_invalid_git_attributes + end + + def create_bare_seeds + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}), + chdir: SEED_REPOSITORY_PATH, + out: '/dev/null', + err: '/dev/null') + end + + def create_normal_seeds + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}), + out: '/dev/null', + err: '/dev/null') + end + + def create_mutable_seeds + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), + out: '/dev/null', + err: '/dev/null') + + system(git_env, *%w(git branch -t feature origin/feature), + chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') + + system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}), + chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') + end + + def create_broken_seeds + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}), + out: '/dev/null', + err: '/dev/null') + + refs_path = File.join(TEST_BROKEN_REPO_PATH, 'refs') + + FileUtils.rm_r(refs_path) + end + + def create_git_attributes + dir = File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git', 'info') + + FileUtils.mkdir_p(dir) + + File.open(File.join(dir, 'attributes'), 'w') do |handle| + handle.write <<-EOF.strip +# This is a comment, it should be ignored. + +*.txt text +*.jpg -text +*.sh eol=lf gitlab-language=shell +*.haml.* gitlab-language=haml +foo/bar.* foo +*.cgi key=value?p1=v1&p2=v2 +/*.png gitlab-language=png +*.binary binary + +# This uses a tab instead of spaces to ensure the parser also supports this. +*.md\tgitlab-language=markdown +bla/bla.txt + EOF + end + end + + def create_invalid_git_attributes + dir = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git', 'info') + + FileUtils.mkdir_p(dir) + + enc = Encoding::UTF_16 + + File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle| + handle.write('# hello'.encode(enc)) + end + end + + # Prevent developer git configurations from being persisted to test + # repositories + def git_env + { 'GIT_TEMPLATE_DIR' => '' } + end +end + +RSpec.configure do |config| + config.include SeedHelper, :seed_helper + + config.before(:all, :seed_helper) do + ensure_seeds + end +end diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb new file mode 100644 index 0000000000000000000000000000000000000000..9f2cd7c67c5042faa503762717e8d25e24e7fe5a --- /dev/null +++ b/spec/support/seed_repo.rb @@ -0,0 +1,143 @@ +# Seed repo: +# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master' +# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers +# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file +# b0e52af38d7ea43cf41d8a6f2471351ac036d6c9 Empty commit +# 40f4a7a617393735a95a0bb67b08385bc1e7c66d Add ISO-8859-encoded file +# 66028349a123e695b589e09a36634d976edcc5e8 Merge branch 'add-comments-to-gitmodules' into 'master' +# de5714f34c4e34f1d50b9a61a2e6c9132fe2b5fd Add comments to the end of .gitmodules to test parsing +# fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 Merge branch 'add-files' into 'master' +# eb49186cfa5c4338011f5f590fac11bd66c5c631 Add submodules nested deeper than the root +# 18d9c205d0d22fdf62bc2f899443b83aafbf941f Add executables and links files +# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com +# 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files +# 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules +# d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files +# c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files +# ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added +# 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified +# 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image +# 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added +# 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more +# cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule +# 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide +# 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit + +module SeedRepo + module BigCommit + ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e" + PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660" + MESSAGE = "Files, encoding and much more" + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" + FILES_COUNT = 2 + end + + module Commit + ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" + FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"] + FILES_COUNT = 2 + C_FILE_PATH = "files/ruby" + C_FILES = ["popen.rb", "regex.rb", "version_info.rb"] + BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n} + BLOB_FILE_PATH = "app/views/keys/show.html.haml" + end + + module EmptyCommit + ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9" + PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d" + MESSAGE = "Empty commit" + AUTHOR_FULL_NAME = "Rémy Coutable" + FILES = [] + FILES_COUNT = FILES.count + end + + module EncodingCommit + ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d" + PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8" + MESSAGE = "Add ISO-8859-encoded file" + AUTHOR_FULL_NAME = "Stan Hu" + FILES = ["encoding/iso8859.txt"] + FILES_COUNT = FILES.count + end + + module FirstCommit + ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863" + PARENT_ID = nil + MESSAGE = "Initial commit" + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" + FILES = ["LICENSE", ".gitignore", "README.md"] + FILES_COUNT = 3 + end + + module LastCommit + ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6" + PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9" + MESSAGE = "Merge branch 'master' into 'master'" + AUTHOR_FULL_NAME = "Stan Hu" + FILES = ["bin/executable"] + FILES_COUNT = FILES.count + end + + module Repo + HEAD = "master" + BRANCHES = %w[ + feature + fix + fix-blob-path + fix-existing-submodule-dir + fix-mode + gitattributes + gitattributes-updated + master + merge-test + ] + TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1] + end + + module RubyBlob + ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c" + NAME = "popen.rb" + CONTENT = <<-eos +require 'fileutils' +require 'open3' + +module Popen + extend self + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + raise RuntimeError, "System commands must be given as an array of strings" + end + + path ||= Dir.pwd + + vars = { + "PWD" => path + } + + options = { + chdir: path + } + + unless File.directory?(path) + FileUtils.mkdir_p(path) + end + + @cmd_output = "" + @cmd_status = 0 + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read + @cmd_status = wait_thr.value.exitstatus + end + + return @cmd_output, @cmd_status + end +end + eos + end +end diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..93c0267d2db0d77d30b217e6764f04a55bca0f2c --- /dev/null +++ b/spec/support/services/issuable_create_service_shared_examples.rb @@ -0,0 +1,52 @@ +shared_examples 'issuable create service' do + context 'asssignee_id' do + let(:assignee) { create(:user) } + + before { project.team << [user, :master] } + + it 'removes assignee_id when user id is invalid' do + opts = { title: 'Title', description: 'Description', assignee_id: -1 } + + issuable = described_class.new(project, user, opts).execute + + expect(issuable.assignee_id).to be_nil + end + + it 'removes assignee_id when user id is 0' do + opts = { title: 'Title', description: 'Description', assignee_id: 0 } + + issuable = described_class.new(project, user, opts).execute + + expect(issuable.assignee_id).to be_nil + end + + it 'saves assignee when user id is valid' do + project.team << [assignee, :master] + opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + + issuable = described_class.new(project, user, opts).execute + + expect(issuable.assignee_id).to eq(assignee.id) + end + + context "when issuable feature is private" do + before do + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) + project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] + + levels.each do |level| + it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do + project.update(visibility_level: level) + opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + + issuable = described_class.new(project, user, opts).execute + + expect(issuable.assignee_id).to be_nil + end + end + end + end +end diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index 5f9645ed44f4c59aa0c98e90f5f060faa42d8498..dd54b0addda68d4facefaa9291a2d87d08e31b3f 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb @@ -11,6 +11,8 @@ shared_examples 'new issuable record that supports slash commands' do let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) } let(:issuable) { described_class.new(project, user, params).execute } + before { project.team << [assignee, :master ] } + context 'with labels in command only' do let(:example_params) do { @@ -55,7 +57,7 @@ shared_examples 'new issuable record that supports slash commands' do context 'with assignee and milestone in params and command' do let(:example_params) do { - assignee: build_stubbed(:user), + assignee: create(:user), milestone_id: double(:milestone), description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}") } diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..49cea1e608cd98b7fa4bf478131da99a1d27c32a --- /dev/null +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -0,0 +1,69 @@ +shared_examples 'issuable update service' do + def update_issuable(opts) + described_class.new(project, user, opts).execute(open_issuable) + end + + context 'changing state' do + before { expect(project).to receive(:execute_hooks).once } + + context 'to reopened' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'reopen').execute(closed_issuable) + end + end + + context 'to closed' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'close').execute(open_issuable) + end + end + end + + context 'asssignee_id' do + it 'does not update assignee when assignee_id is invalid' do + open_issuable.update(assignee_id: user.id) + + update_issuable(assignee_id: -1) + + expect(open_issuable.reload.assignee).to eq(user) + end + + it 'unassigns assignee when user id is 0' do + open_issuable.update(assignee_id: user.id) + + update_issuable(assignee_id: 0) + + expect(open_issuable.assignee_id).to be_nil + end + + it 'saves assignee when user id is valid' do + update_issuable(assignee_id: user.id) + + expect(open_issuable.assignee_id).to eq(user.id) + end + + it 'does not update assignee_id when user cannot read issue' do + non_member = create(:user) + original_assignee = open_issuable.assignee + + update_issuable(assignee_id: non_member.id) + + expect(open_issuable.assignee_id).to eq(original_assignee.id) + end + + context "when issuable feature is private" do + levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] + + levels.each do |level| + it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do + assignee = create(:user) + project.update(visibility_level: level) + feature_visibility_attr = :"#{open_issuable.model_name.plural}_access_level" + project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) + + expect{ update_issuable(assignee_id: assignee) }.not_to change{ open_issuable.assignee } + end + end + end + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..74d9b8c6313094acec5413812521e7db0d6d65e8 --- /dev/null +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -0,0 +1,328 @@ +Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } + +RSpec.shared_examples 'slack or mattermost notifications' do + let(:chat_service) { described_class.new } + let(:webhook_url) { 'https://example.gitlab.com/' } + + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:webhook) } + it_behaves_like 'issue tracker service URL attribute', :webhook + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:webhook) } + end + end + + describe "#execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + + let(:push_sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + before do + allow(chat_service).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') + + opts = { + title: "Awesome wiki_page", + content: "Some text describing some thing or another", + format: "md", + message: "user created page: Awesome wiki_page" + } + + wiki_page_service = WikiPages::CreateService.new(project, user, opts) + @wiki_page = wiki_page_service.execute + @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') + end + + it "calls Slack/Mattermost API for push events" do + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for issue events" do + chat_service.execute(@issues_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for merge requests events" do + chat_service.execute(@merge_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for wiki page events" do + chat_service.execute(@wiki_page_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it 'uses the username as an option for slack when configured' do + allow(chat_service).to receive(:username).and_return(username) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, username: username). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it 'uses the channel as an option when it is configured' do + allow(chat_service).to receive(:channel).and_return(channel) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: channel). + and_return( + double(:slack_service).as_null_object + ) + chat_service.execute(push_sample_data) + end + + context "event channels" do + it "uses the right channel for push event" do + chat_service.update_attributes(push_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it "uses the right channel for merge request event" do + chat_service.update_attributes(merge_request_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@merge_sample_data) + end + + it "uses the right channel for issue event" do + chat_service.update_attributes(issue_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@issues_sample_data) + end + + it "uses the right channel for wiki event" do + chat_service.update_attributes(wiki_page_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@wiki_page_sample_data) + end + + context "note event" do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "uses the right channel" do + chat_service.update_attributes(note_channel: "random") + + note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(note_data) + end + end + end + end + + describe "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + + before do + allow(chat_service).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + context 'when commit comment event executed' do + let(:commit_note) do + create(:note_on_commit, author: user, + project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit') + end + + it "calls Slack/Mattermost API for commit comment events" do + data = Gitlab::DataBuilder::Note.build(commit_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when merge request comment event executed' do + let(:merge_request_note) do + create(:note_on_merge_request, project: project, + note: "merge request note") + end + + it "calls Slack API for merge request comment events" do + data = Gitlab::DataBuilder::Note.build(merge_request_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when issue comment event executed' do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "calls Slack API for issue comment events" do + data = Gitlab::DataBuilder::Note.build(issue_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when snippet comment event executed' do + let(:snippet_note) do + create(:note_on_project_snippet, project: project, + note: "snippet note") + end + + it "calls Slack API for snippet comment events" do + data = Gitlab::DataBuilder::Note.build(snippet_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + end + + describe 'Pipeline events' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: status, + sha: project.commit.sha, ref: project.default_branch) + end + + before do + allow(chat_service).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + end + + shared_examples 'call Slack/Mattermost API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + + it_behaves_like 'call Slack/Mattermost API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = chat_service.execute(data) + + expect(result).to be_falsy + end + end + + context 'with setting notify_only_broken_pipelines to false' do + before do + chat_service.notify_only_broken_pipelines = false + end + + it_behaves_like 'call Slack/Mattermost API' + end + end + end +end diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb new file mode 100644 index 0000000000000000000000000000000000000000..18597b5c71f7c481fc4bfef64c9494cb7f9452e7 --- /dev/null +++ b/spec/support/stub_env.rb @@ -0,0 +1,7 @@ +module StubENV + def stub_env(key, value) + allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed + @env_already_stubbed ||= true + allow(ENV).to receive(:[]).with(key).and_return(value) + end +end diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..02657684b5733890160ecefe2f2b77400751bd8a --- /dev/null +++ b/spec/support/time_tracking_shared_examples.rb @@ -0,0 +1,82 @@ +shared_examples 'issuable time tracker' do + it 'renders the sidebar component empty state' do + page.within '.time-tracking-no-tracking-pane' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when estimate is added' do + submit_time('/estimate 3w 1d 1h') + + page.within '.time-tracking-estimate-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when spent is added' do + submit_time('/spend 3w 1d 1h') + + page.within '.time-tracking-spend-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'shows the comparison when estimate and spent are added' do + submit_time('/estimate 3w 1d 1h') + submit_time('/spend 3w 1d 1h') + + page.within '.time-tracking-comparison-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when estimate is removed' do + submit_time('/estimate 3w 1d 1h') + submit_time('/remove_estimate') + + page.within '#issuable-time-tracker' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when spent is removed' do + submit_time('/spend 3w 1d 1h') + submit_time('/remove_time_spent') + + page.within '#issuable-time-tracker' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'shows the help state when icon is clicked' do + page.within '#issuable-time-tracker' do + find('.help-button').click + expect(page).to have_content 'Track time with slash commands' + expect(page).to have_content 'Learn more' + end + end + + it 'hides the help state when close icon is clicked' do + page.within '#issuable-time-tracker' do + find('.help-button').click + find('.close-help-button').click + + expect(page).not_to have_content 'Track time with slash commands' + expect(page).not_to have_content 'Learn more' + end + end + + it 'displays the correct help url' do + page.within '#issuable-time-tracker' do + find('.help-button').click + + expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') + end + end +end + +def submit_time(slash_command) + fill_in 'note[note]', with: slash_command + click_button 'Comment' + wait_for_ajax +end diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..5eead80c935d7121d1f3a8dd2df2190679d69624 --- /dev/null +++ b/spec/support/upload_helpers.rb @@ -0,0 +1,16 @@ +require 'fileutils' + +module UploadHelpers + extend self + + def uploaded_image_temp_path + basename = 'banana_sample.gif' + orig_path = File.join(Rails.root, 'spec', 'fixtures', basename) + tmp_path = File.join(Rails.root, 'tmp', 'tests', basename) + # Because we use 'move_to_store' on all uploaders, we create a new + # tempfile on each call: the file we return here will be renamed in most + # cases. + FileUtils.copy(orig_path, tmp_path) + tmp_path + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index a9fea5f1e81943377d29e29dc434a5a155a25cff..bc751d20ce11c90a1ac5be428c53033ca90508e6 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -41,7 +41,7 @@ describe 'gitlab:app namespace rake task' do context 'gitlab version' do before do - allow(Dir).to receive(:glob).and_return([]) + allow(Dir).to receive(:glob).and_return(['1_gitlab_backup.tar']) allow(Dir).to receive(:chdir) allow(File).to receive(:exist?).and_return(true) allow(Kernel).to receive(:system).and_return(true) diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..12d442b982034f7f64dfae5dc01e2913b1085c6c --- /dev/null +++ b/spec/tasks/gitlab/ldap_rake_spec.rb @@ -0,0 +1,13 @@ +require 'rake_helper' + +describe 'gitlab:ldap:rename_provider rake task' do + it 'completes without error' do + Rake.application.rake_require 'tasks/gitlab/ldap' + stub_warn_user_is_not_gitlab + ENV['force'] = 'yes' + + create(:identity) # Necessary to prevent `exit 1` from the task. + + run_rake_task('gitlab:ldap:rename_provider', 'ldapmain', 'ldapfoo') + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6098be5cd45508848298b28c4016fd0ccb74b8aa --- /dev/null +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AttachmentUploader do + let(:issue) { build(:issue) } + subject { described_class.new(issue) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..76f5a4b42ed024dd754c2f2e9046df3845cdda18 --- /dev/null +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AvatarUploader do + let(:user) { build(:user) } + subject { described_class.new(user) } + + describe '#move_to_cache' do + it 'is false' do + expect(subject.move_to_cache).to eq(false) + end + end + + describe '#move_to_store' do + it 'is false' do + expect(subject.move_to_store).to eq(false) + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index e8300abed5dc201ecf5cc4e01e2afe0d3a661a9d..6a712e33c962caa6543d35f673b1a9adf45e5dfc 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -17,7 +17,7 @@ describe FileUploader do describe '#image_or_video?' do context 'given an image file' do before do - @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) + @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) end it 'detects an image based on file extension' do @@ -27,7 +27,7 @@ describe FileUploader do context 'given an video file' do before do - video_file = File.new(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) + video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) @uploader.store!(video_file) end @@ -37,9 +37,21 @@ describe FileUploader do end it 'does not return image_or_video? for other types' do - @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) expect(@uploader.image_or_video?).to be false end end + + describe '#move_to_cache' do + it 'is true' do + expect(@uploader.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(@uploader.move_to_store).to eq(true) + end + end end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 33cabd14913a0ee70c60a46cdc9e97db30db460b..7f123b15194788f036e544bf5e11ee309e332958 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -7,6 +7,7 @@ describe 'projects/merge_requests/show.html.haml' do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) } let(:closed_merge_request) do create(:closed_merge_request, @@ -19,8 +20,12 @@ describe 'projects/merge_requests/show.html.haml' do assign(:project, project) assign(:merge_request, closed_merge_request) assign(:commits_count, 0) + assign(:note, note) + assign(:noteable, closed_merge_request) + assign(:notes, []) + assign(:pipelines, Ci::Pipeline.none) - allow(view).to receive(:can?).and_return(true) + allow(view).to receive_messages(current_user: user, can?: true) end context 'when the merge request is closed' do diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d25de8af5d2f9087d281f270c8940174a8224de2 --- /dev/null +++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'projects/pipelines/_stage', :view do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:stage) { build(:ci_stage, pipeline: pipeline) } + + before do + assign :stage, stage + end + + context 'when there are only latest builds present' do + before do + create(:ci_build, name: 'test:build', + stage: stage.name, + pipeline: pipeline) + end + + it 'shows the builds in the stage' do + render + + expect(rendered).to have_text 'test:build' + end + end + + context 'when build belongs to different stage' do + before do + create(:ci_build, name: 'test:build', + stage: 'other:stage', + pipeline: pipeline) + end + + it 'does not render build' do + render + + expect(rendered).not_to have_text 'test:build' + end + end + + context 'when there are retried builds present' do + before do + create_list(:ci_build, 2, name: 'test:build', + stage: stage.name, + pipeline: pipeline) + end + + it 'shows only latest builds' do + render + + expect(rendered).to have_text 'test:build', count: 1 + end + end +end diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index a066ea078e65bd704aa066e50a8ccd6f9342af75..c101f6f164dffc9aa6b753d79e288914ac6315db 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -29,7 +29,7 @@ describe 'projects/pipelines/show' do render expect(rendered).to have_css('.js-pipeline-graph') - expect(rendered).to have_css('.grouped-pipeline-dropdown') + expect(rendered).to have_css('.js-grouped-pipeline-dropdown') # stages expect(rendered).to have_text('Build') diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb new file mode 100644 index 0000000000000000000000000000000000000000..4769d569548840cd2e55fcf9e3b52ff26632bdff --- /dev/null +++ b/spec/views/shared/milestones/_issuables.html.haml.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'shared/milestones/_issuables.html.haml' do + let(:issuables_size) { 100 } + + before do + allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil, + show_full_project_name: nil, dom_class: '', + issuables: double(size: issuables_size).as_null_object) + + stub_template 'shared/milestones/_issuable.html.haml' => '' + end + + it 'should show the issuables count if show_counter is true' do + render 'shared/milestones/issuables', show_counter: true + expect(rendered).to have_content('100') + end + + it 'should not show the issuables count if show_counter is false' do + render 'shared/milestones/issuables', show_counter: false + expect(rendered).not_to have_content('100') + end + + describe 'a high issuables count' do + let(:issuables_size) { 1000 } + + it 'should show a delimited number if show_counter is true' do + render 'shared/milestones/issuables', show_counter: true + expect(rendered).to have_content('1,000') + end + end +end diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 95e2458da35f28dceeee2db64a93ddcb2afe8f98..b6591f272f6159d1c5cd1d72115ad36824823db0 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -7,27 +7,17 @@ describe AuthorizedProjectsWorker do it "refreshes user's authorized projects" do user = create(:user) - expect(worker).to receive(:refresh).with(an_instance_of(User)) + expect_any_instance_of(User).to receive(:refresh_authorized_projects) worker.perform(user.id) end context "when the user is not found" do it "does nothing" do - expect(worker).not_to receive(:refresh) + expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) described_class.new.perform(-1) end end end - - describe '#refresh', redis: true do - it 'refreshes the authorized projects of the user' do - user = create(:user) - - expect(user).to receive(:refresh_authorized_projects) - - worker.refresh(user) - end - end end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 855c28b584e0ee3a2297b608757c3598b9a45b9a..f4f63b57a5fb26afc31714395391e308e439f7b7 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe ProjectCacheWorker do - let(:project) { create(:project) } let(:worker) { described_class.new } + let(:project) { create(:project) } + let(:statistics) { project.statistics } describe '#perform' do before do @@ -12,7 +13,7 @@ describe ProjectCacheWorker do context 'with a non-existing project' do it 'does nothing' do - expect(worker).not_to receive(:update_repository_size) + expect(worker).not_to receive(:update_statistics) worker.perform(-1) end @@ -22,24 +23,19 @@ describe ProjectCacheWorker do it 'does nothing' do allow_any_instance_of(Repository).to receive(:exists?).and_return(false) - expect(worker).not_to receive(:update_repository_size) + expect(worker).not_to receive(:update_statistics) worker.perform(project.id) end end context 'with an existing project' do - it 'updates the repository size' do - expect(worker).to receive(:update_repository_size).and_call_original - - worker.perform(project.id) - end - - it 'updates the commit count' do - expect_any_instance_of(Project).to receive(:update_commit_count). - and_call_original + it 'updates the project statistics' do + expect(worker).to receive(:update_statistics) + .with(kind_of(Project), %i(repository_size)) + .and_call_original - worker.perform(project.id) + worker.perform(project.id, [], %w(repository_size)) end it 'refreshes the method caches' do @@ -47,33 +43,35 @@ describe ProjectCacheWorker do with(%i(readme)). and_call_original - worker.perform(project.id, %i(readme)) + worker.perform(project.id, %w(readme)) end end end - describe '#update_repository_size' do + describe '#update_statistics' do context 'when a lease could not be obtained' do it 'does not update the repository size' do allow(worker).to receive(:try_obtain_lease_for). - with(project.id, :update_repository_size). + with(project.id, :update_statistics). and_return(false) - expect(project).not_to receive(:update_repository_size) + expect(statistics).not_to receive(:refresh!) - worker.update_repository_size(project) + worker.update_statistics(project) end end context 'when a lease could be obtained' do - it 'updates the repository size' do + it 'updates the project statistics' do allow(worker).to receive(:try_obtain_lease_for). - with(project.id, :update_repository_size). + with(project.id, :update_statistics). and_return(true) - expect(project).to receive(:update_repository_size).and_call_original + expect(statistics).to receive(:refresh!) + .with(only: %i(repository_size)) + .and_call_original - worker.update_repository_size(project) + worker.update_statistics(project, %i(repository_size)) end end end diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5f4453c15d63cd5b30efef3372fa8030cf4c899b --- /dev/null +++ b/spec/workers/reactive_caching_worker_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe ReactiveCachingWorker do + let(:project) { create(:kubernetes_project) } + let(:service) { project.deployment_service } + subject { described_class.new.perform("KubernetesService", service.id) } + + describe '#perform' do + it 'calls #exclusively_update_reactive_cache!' do + expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!) + + subject + end + end +end diff --git a/spec/workers/use_key_worker_spec.rb b/spec/workers/use_key_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e50c788b82aa8172b1c38f6987f4990d62616827 --- /dev/null +++ b/spec/workers/use_key_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe UseKeyWorker do + describe "#perform" do + it "updates the key's last_used_at attribute to the current time when it exists" do + worker = described_class.new + key = create(:key) + current_time = Time.zone.now + + Timecop.freeze(current_time) do + expect { worker.perform(key.id) } + .to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time) + end + end + + it "returns false and skips the job when the key doesn't exist" do + worker = described_class.new + key = create(:key) + + expect(worker.perform(key.id + 1)).to eq false + end + end +end diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.eot b/vendor/assets/fonts/KaTeX_AMS-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..784276a3cbfb3d3b93c974594ffaf70514ab0eb7 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.ttf b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6f1e0be2028f8c9ccb9ca055f9091fd8495c2975 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff b/vendor/assets/fonts/KaTeX_AMS-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..4dded4733b3fd85fdb1c00352e30d8efa3f1a426 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ea81079c4e2ec7160db9fe41ffa1ab70098f3d45 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot new file mode 100644 index 0000000000000000000000000000000000000000..1a0db0c568e4697d56785a687649e98e9bacda7b Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b94907dad11e545adaf8fb1aaf54c032b7de1796 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..799fa8122ca184fff98b8f0424ae6949f8668984 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..73bb54228786739b75b05af392000dd214486580 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..6cc83d0922c961a8e94dc0460bb583f14b6a041e Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..cf51e2021e4d84b7fa8d19820387742138b5aedb Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..f5e5c623577a08e9c85da33e2bd383e32fe18c28 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..dd76d3488d5b1722e8f929349a978d8cd953c4c2 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot new file mode 100644 index 0000000000000000000000000000000000000000..1960b106656db1de5df7206db47cb445c8e906dc Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7b0790f1ae89994c5e7a54b219c013f80166cc18 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..dc325713291c91c9fef5967e0d52d0366704715e Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..fdc429227adb718aabf201bed2075bfbaff70a75 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..e4e73796aea73b5a7bf372f36a27d6703679a7ca Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..063bc0263eba6fff6ab62d728b5ef666b134f5e1 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..c4b18d863f31c568adb0b8de64ff2368cad1b656 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4318d938e26c1dd40212772f4c9984b7fbce6adf Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.eot b/vendor/assets/fonts/KaTeX_Main-Bold.eot new file mode 100644 index 0000000000000000000000000000000000000000..80fbd022363e7ee84f211c9f814ffa479d588815 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.ttf b/vendor/assets/fonts/KaTeX_Main-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8e10722afae5f5f0d9ac4dc5245d445db9151f2c Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff b/vendor/assets/fonts/KaTeX_Main-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..43b361a6005e845717c8de2fbc8980830acfceb2 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff2 b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..af57a96c1488802f4a5a3d59cac03ad04860ea84 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.eot b/vendor/assets/fonts/KaTeX_Main-Italic.eot new file mode 100644 index 0000000000000000000000000000000000000000..fc770166b5e86b6a9c18440231ec09449c89b634 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.eot differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.ttf b/vendor/assets/fonts/KaTeX_Main-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d124495d7b68caa27fb15435bf0004bfe058e5a6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff b/vendor/assets/fonts/KaTeX_Main-Italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..e623236bc44485389e5a1d77937699967818bbca Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.woff differ diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff2 b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..944e9740bdf61ca4f437f48f44133253c7caf052 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.eot b/vendor/assets/fonts/KaTeX_Main-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..dc60c090c7a31a74ffa16acea85ddb4e715ccc5f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.ttf b/vendor/assets/fonts/KaTeX_Main-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..da5797ffcce7d6bcf6155a5744398fc92ed15cc4 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff b/vendor/assets/fonts/KaTeX_Main-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..37db672e821041b2232c741848d4a37052fda363 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff2 b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4882042489387f004bbfd83fa1aaa68d3655cc48 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot new file mode 100644 index 0000000000000000000000000000000000000000..52c8b8c6b4089c75de5f6f82635154be2188afcb Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a8b527c7ef6b56119ef869c96947eb0f3228c20f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff new file mode 100644 index 0000000000000000000000000000000000000000..8940e0b58013e7002cc8af7f77c2ad78460f9411 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff differ diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..15cf56d3408b3d1afa9ccae05dae5de94e91cdce Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.eot b/vendor/assets/fonts/KaTeX_Math-Italic.eot new file mode 100644 index 0000000000000000000000000000000000000000..64c8992c477ed0007844d3cfd3f0f962eb91ac89 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.eot differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.ttf b/vendor/assets/fonts/KaTeX_Math-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..06f39d3a29906ba9c26675cb57188dd25a57609d Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff b/vendor/assets/fonts/KaTeX_Math-Italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..cf3b4b79e5b940e9e7a48b79022e1ed82fcf2786 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.woff differ diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff2 b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5f8c4bfa455259e3e0a1c53c8fa399fe2a4ead59 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.eot b/vendor/assets/fonts/KaTeX_Math-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..5521e6a564de3076d4b790a9db8eb302333a2916 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.ttf b/vendor/assets/fonts/KaTeX_Math-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..731270823704787f06e136c49e588614fa39ab68 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff b/vendor/assets/fonts/KaTeX_Math-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..0e2ebdf18af76d3f8af92b55f7c76a60e30a9c08 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff2 b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ebe3d028a34a343fe4d31706cf3d2644d646cfad Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot new file mode 100644 index 0000000000000000000000000000000000000000..1660e76a2b6ea179340472720b3cbd15acf1ce9f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dbeb7b92ab57a2687b8bcd80350520205127b6bc Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..8f144a8bb312fa160afff853133665554eee9065 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..329e85557fa22c84800d38d8e0d28e30bbd6543f Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot new file mode 100644 index 0000000000000000000000000000000000000000..289ae3ff8b793b05b342aa29ff2d7c0447196368 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b3a2f38f22466e837cc0a1e8bfe166ab86787496 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..bddf7ea6579bc13bd4455a14c5f380e69cbd6b50 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5fa767bddd69dde2a3a8a009667c5555161058ef Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..1b38b98a180c93c0dee50f491c520265029a98e9 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e4712f847754250be2596ca801da591b0f558416 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..33be368048f7e28c8b87fe4631339a801a09b871 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4fcb2e29a05c2a1c1894982b7d63d916a755faac Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.eot b/vendor/assets/fonts/KaTeX_Script-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..7870d7f319b6efba6bba9502db33fb4b73129403 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.ttf b/vendor/assets/fonts/KaTeX_Script-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..da4d11308aeecb950b6a4f669ef1d4b05746f887 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff b/vendor/assets/fonts/KaTeX_Script-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..d6ae79f998a341c1b0fc5a9292ca8403f734d23d Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff2 b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1b43deb45a854d69d134865608f7ddf128e654c3 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.eot b/vendor/assets/fonts/KaTeX_Size1-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..29950f95ff6a625cfe0435b01d5fbe6b6270c938 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.ttf b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..194466a655d38d8c0c112b39e7b04c5182670c6d Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff b/vendor/assets/fonts/KaTeX_Size1-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..237f271edd1ca85a9cb78d12c142f22deec6788e Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..39b6f8f746c39c8c2faa9188de664475443d6b91 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.eot b/vendor/assets/fonts/KaTeX_Size2-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..b8b0536f96767e3e4235d7795f5bb5bb6b7d03f6 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.ttf b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b41b66a638ff64d009950af0a9463561ba552290 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff b/vendor/assets/fonts/KaTeX_Size2-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..4a3055854ede4eaf25de2364da0fea0a05858cfc Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3facec1ab899c8b984146e54051efafb948eb08c Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.eot b/vendor/assets/fonts/KaTeX_Size3-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..576b864fae6e163967a7fbe5f6da41c8076d6210 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.ttf b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..790ddbbc55fdd3e4bf42f567e4f5f3b7aee5c86a Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff b/vendor/assets/fonts/KaTeX_Size3-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..3a6d062e660d3a9f71b0230e601dcaff83638430 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2cffafe5018f6e8a7988d6e277d2cf2177968aca Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.eot b/vendor/assets/fonts/KaTeX_Size4-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..c2b045fc3dbebff9ecede9a4a1fcf9080633fb15 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.ttf b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ce660aa7ff91c74b859c521bded76a3fe2150b5b Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff b/vendor/assets/fonts/KaTeX_Size4-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..7826c6c97a108759a64d1fd402428ad73c795898 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c92189812d98fdf4feb8eafcc81481778f039f65 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..4c178f484a8d7369a4142755c30ae4a0e12e8d5d Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b0427ad0a56ca0eb58c035ebadbc7bc3ebe7fae0 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..78e990488a9b61630e28263b73157205c0aeda96 Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff differ diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..618de99d480f9a5dfeabce556f3a94bd31de1a2c Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 differ diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js new file mode 100644 index 0000000000000000000000000000000000000000..fd6e95e75d50e198044ee164dcac4b0c15264fac --- /dev/null +++ b/vendor/assets/javascripts/jquery.turbolinks.js @@ -0,0 +1,49 @@ +// Generated by CoffeeScript 1.7.1 + +/* +jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks +jQuery plugin for drop-in fix binded events problem caused by Turbolinks + +The MIT License +Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz + */ + +(function() { + var $, $document; + + $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); + + $document = $(document); + + $.turbo = { + version: '2.1.0', + isReady: false, + use: function(load, fetch) { + return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch); + }, + addCallback: function(callback) { + if ($.turbo.isReady) { + callback($); + } + return $document.on('turbo:ready', function() { + return callback($); + }); + }, + onLoad: function() { + $.turbo.isReady = true; + return $document.trigger('turbo:ready'); + }, + onFetch: function() { + return $.turbo.isReady = false; + }, + register: function() { + $(this.onLoad); + return $.fn.ready = this.addCallback; + } + }; + + $.turbo.register(); + + $.turbo.use('page:load', 'page:fetch'); + +}).call(this); diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js new file mode 100644 index 0000000000000000000000000000000000000000..6b59a3477a70e6c79c9d366bca54f3d616c6db7a --- /dev/null +++ b/vendor/assets/javascripts/katex.js @@ -0,0 +1,8685 @@ +/* + The MIT License (MIT) + + Copyright (c) 2015 Khan Academy + + This software also uses portions of the underscore.js project, which is + MIT licensed with the following copyright: + + Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative + Reporters & Editors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/* + Here is how to build a version of KaTeX that works with gitlab. + + The problem is that the standard procedure for changing font location doesn't work for the empty string. + + 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. + 2. make (requires node) + 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss + 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js, + build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and + fonts/* to gitlab/vendor/assets/fonts/. +*/ + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This is the main entry point for KaTeX. Here, we expose functions for + * rendering expressions either to DOM nodes or to markup strings. + * + * We also expose the ParseError class to check if errors thrown from KaTeX are + * errors in the expression, or errors in javascript handling. + */ + +var ParseError = require("./src/ParseError"); +var Settings = require("./src/Settings"); + +var buildTree = require("./src/buildTree"); +var parseTree = require("./src/parseTree"); +var utils = require("./src/utils"); + +/** + * Parse and build an expression, and place that expression in the DOM node + * given. + */ +var render = function(expression, baseNode, options) { + utils.clearNode(baseNode); + + var settings = new Settings(options); + + var tree = parseTree(expression, settings); + var node = buildTree(tree, expression, settings).toNode(); + + baseNode.appendChild(node); +}; + +// KaTeX's styles don't work properly in quirks mode. Print out an error, and +// disable rendering. +if (typeof document !== "undefined") { + if (document.compatMode !== "CSS1Compat") { + typeof console !== "undefined" && console.warn( + "Warning: KaTeX doesn't work in quirks mode. Make sure your " + + "website has a suitable doctype."); + + render = function() { + throw new ParseError("KaTeX doesn't work in quirks mode."); + }; + } +} + +/** + * Parse and build an expression, and return the markup for that. + */ +var renderToString = function(expression, options) { + var settings = new Settings(options); + + var tree = parseTree(expression, settings); + return buildTree(tree, expression, settings).toMarkup(); +}; + +/** + * Parse an expression and return the parse tree. + */ +var generateParseTree = function(expression, options) { + var settings = new Settings(options); + return parseTree(expression, settings); +}; + +module.exports = { + render: render, + renderToString: renderToString, + /** + * NOTE: This method is not currently recommended for public use. + * The internal tree representation is unstable and is very likely + * to change. Use at your own risk. + */ + __parse: generateParseTree, + ParseError: ParseError, +}; + +},{"./src/ParseError":6,"./src/Settings":8,"./src/buildTree":13,"./src/parseTree":22,"./src/utils":25}],2:[function(require,module,exports){ +/** @flow */ + +"use strict"; + +function getRelocatable(re) { + // In the future, this could use a WeakMap instead of an expando. + if (!re.__matchAtRelocatable) { + // Disjunctions are the lowest-precedence operator, so we can make any + // pattern match the empty string by appending `|()` to it: + // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-patterns + var source = re.source + "|()"; + + // We always make the new regex global. + var flags = "g" + (re.ignoreCase ? "i" : "") + (re.multiline ? "m" : "") + (re.unicode ? "u" : "") + // sticky (/.../y) doesn't make sense in conjunction with our relocation + // logic, so we ignore it here. + ; + + re.__matchAtRelocatable = new RegExp(source, flags); + } + return re.__matchAtRelocatable; +} + +function matchAt(re, str, pos) { + if (re.global || re.sticky) { + throw new Error("matchAt(...): Only non-global regexes are supported"); + } + var reloc = getRelocatable(re); + reloc.lastIndex = pos; + var match = reloc.exec(str); + // Last capturing group is our sentinel that indicates whether the regex + // matched at the given location. + if (match[match.length - 1] == null) { + // Original regex matched. + match.length = match.length - 1; + return match; + } else { + return null; + } +} + +module.exports = matchAt; +},{}],3:[function(require,module,exports){ +/** + * The Lexer class handles tokenizing the input in various ways. Since our + * parser expects us to be able to backtrack, the lexer allows lexing from any + * given starting point. + * + * Its main exposed function is the `lex` function, which takes a position to + * lex from and a type of token to lex. It defers to the appropriate `_innerLex` + * function. + * + * The various `_innerLex` functions perform the actual lexing of different + * kinds. + */ + +var matchAt = require("match-at"); + +var ParseError = require("./ParseError"); + +// The main lexer class +function Lexer(input) { + this.input = input; + this.pos = 0; +} + +/** + * The resulting token returned from `lex`. + * + * It consists of the token text plus some position information. + * The position information is essentially a range in an input string, + * but instead of referencing the bare input string, we refer to the lexer. + * That way it is possible to attach extra metadata to the input string, + * like for example a file name or similar. + * + * The position information (all three parameters) is optional, + * so it is OK to construct synthetic tokens if appropriate. + * Not providing available position information may lead to + * degraded error reporting, though. + * + * @param {string} text the text of this token + * @param {number=} start the start offset, zero-based inclusive + * @param {number=} end the end offset, zero-based exclusive + * @param {Lexer=} lexer the lexer which in turn holds the input string + */ +function Token(text, start, end, lexer) { + this.text = text; + this.start = start; + this.end = end; + this.lexer = lexer; +} + +/** + * Given a pair of tokens (this and endToken), compute a “Token†encompassing + * the whole input range enclosed by these two. + * + * @param {Token} endToken last token of the range, inclusive + * @param {string} text the text of the newly constructed token + */ +Token.prototype.range = function(endToken, text) { + if (endToken.lexer !== this.lexer) { + return new Token(text); // sorry, no position information available + } + return new Token(text, this.start, endToken.end, this.lexer); +}; + +/* The following tokenRegex + * - matches typical whitespace (but not NBSP etc.) using its first group + * - does not match any control character \x00-\x1f except whitespace + * - does not match a bare backslash + * - matches any ASCII character except those just mentioned + * - does not match the BMP private use area \uE000-\uF8FF + * - does not match bare surrogate code units + * - matches any BMP character except for those just described + * - matches any valid Unicode surrogate pair + * - matches a backslash followed by one or more letters + * - matches a backslash followed by any BMP character, including newline + * Just because the Lexer matches something doesn't mean it's valid input: + * If there is no matching function or symbol definition, the Parser will + * still reject the input. + */ +var tokenRegex = new RegExp( + "([ \r\n\t]+)|" + // whitespace + "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint + "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair + "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name + ")" +); + +/** + * This function lexes a single token. + */ +Lexer.prototype.lex = function() { + var input = this.input; + var pos = this.pos; + if (pos === input.length) { + return new Token("EOF", pos, pos, this); + } + var match = matchAt(tokenRegex, input, pos); + if (match === null) { + throw new ParseError( + "Unexpected character: '" + input[pos] + "'", + new Token(input[pos], pos, pos + 1, this)); + } + var text = match[2] || " "; + var start = this.pos; + this.pos += match[0].length; + var end = this.pos; + return new Token(text, start, end, this); +}; + +module.exports = Lexer; + +},{"./ParseError":6,"match-at":2}],4:[function(require,module,exports){ +/** + * This file contains the “gullet†where macros are expanded + * until only non-macro tokens remain. + */ + +var Lexer = require("./Lexer"); + +function MacroExpander(input, macros) { + this.lexer = new Lexer(input); + this.macros = macros; + this.stack = []; // contains tokens in REVERSE order + this.discardedWhiteSpace = []; +} + +/** + * Recursively expand first token, then return first non-expandable token. + */ +MacroExpander.prototype.nextToken = function() { + for (;;) { + if (this.stack.length === 0) { + this.stack.push(this.lexer.lex()); + } + var topToken = this.stack.pop(); + var name = topToken.text; + if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { + return topToken; + } + var expansion = this.macros[name]; + if (typeof expansion === "string") { + var bodyLexer = new Lexer(expansion); + expansion = []; + var tok = bodyLexer.lex(); + while (tok.text !== "EOF") { + expansion.push(tok); + tok = bodyLexer.lex(); + } + expansion.reverse(); // to fit in with stack using push and pop + this.macros[name] = expansion; + } + this.stack = this.stack.concat(expansion); + } +}; + +MacroExpander.prototype.get = function(ignoreSpace) { + this.discardedWhiteSpace = []; + var token = this.nextToken(); + if (ignoreSpace) { + while (token.text === " ") { + this.discardedWhiteSpace.push(token); + token = this.nextToken(); + } + } + return token; +}; + +/** + * Undo the effect of the preceding call to the get method. + * A call to this method MUST be immediately preceded and immediately followed + * by a call to get. Only used during mode switching, i.e. after one token + * was got in the old mode but should get got again in a new mode + * with possibly different whitespace handling. + */ +MacroExpander.prototype.unget = function(token) { + this.stack.push(token); + while (this.discardedWhiteSpace.length !== 0) { + this.stack.push(this.discardedWhiteSpace.pop()); + } +}; + +module.exports = MacroExpander; + +},{"./Lexer":3}],5:[function(require,module,exports){ +/** + * This file contains information about the options that the Parser carries + * around with it while parsing. Data is held in an `Options` object, and when + * recursing, a new `Options` object can be created with the `.with*` and + * `.reset` functions. + */ + +/** + * This is the main options class. It contains the style, size, color, and font + * of the current parse level. It also contains the style and size of the parent + * parse level, so size changes can be handled efficiently. + * + * Each of the `.with*` and `.reset` functions passes its current style and size + * as the parentStyle and parentSize of the new options class, so parent + * handling is taken care of automatically. + */ +function Options(data) { + this.style = data.style; + this.color = data.color; + this.size = data.size; + this.phantom = data.phantom; + this.font = data.font; + + if (data.parentStyle === undefined) { + this.parentStyle = data.style; + } else { + this.parentStyle = data.parentStyle; + } + + if (data.parentSize === undefined) { + this.parentSize = data.size; + } else { + this.parentSize = data.parentSize; + } +} + +/** + * Returns a new options object with the same properties as "this". Properties + * from "extension" will be copied to the new options object. + */ +Options.prototype.extend = function(extension) { + var data = { + style: this.style, + size: this.size, + color: this.color, + parentStyle: this.style, + parentSize: this.size, + phantom: this.phantom, + font: this.font, + }; + + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + data[key] = extension[key]; + } + } + + return new Options(data); +}; + +/** + * Create a new options object with the given style. + */ +Options.prototype.withStyle = function(style) { + return this.extend({ + style: style, + }); +}; + +/** + * Create a new options object with the given size. + */ +Options.prototype.withSize = function(size) { + return this.extend({ + size: size, + }); +}; + +/** + * Create a new options object with the given color. + */ +Options.prototype.withColor = function(color) { + return this.extend({ + color: color, + }); +}; + +/** + * Create a new options object with "phantom" set to true. + */ +Options.prototype.withPhantom = function() { + return this.extend({ + phantom: true, + }); +}; + +/** + * Create a new options objects with the give font. + */ +Options.prototype.withFont = function(font) { + return this.extend({ + font: font, + }); +}; + +/** + * Create a new options object with the same style, size, and color. This is + * used so that parent style and size changes are handled correctly. + */ +Options.prototype.reset = function() { + return this.extend({}); +}; + +/** + * A map of color names to CSS colors. + * TODO(emily): Remove this when we have real macros + */ +var colorMap = { + "katex-blue": "#6495ed", + "katex-orange": "#ffa500", + "katex-pink": "#ff00af", + "katex-red": "#df0030", + "katex-green": "#28ae7b", + "katex-gray": "gray", + "katex-purple": "#9d38bd", + "katex-blueA": "#ccfaff", + "katex-blueB": "#80f6ff", + "katex-blueC": "#63d9ea", + "katex-blueD": "#11accd", + "katex-blueE": "#0c7f99", + "katex-tealA": "#94fff5", + "katex-tealB": "#26edd5", + "katex-tealC": "#01d1c1", + "katex-tealD": "#01a995", + "katex-tealE": "#208170", + "katex-greenA": "#b6ffb0", + "katex-greenB": "#8af281", + "katex-greenC": "#74cf70", + "katex-greenD": "#1fab54", + "katex-greenE": "#0d923f", + "katex-goldA": "#ffd0a9", + "katex-goldB": "#ffbb71", + "katex-goldC": "#ff9c39", + "katex-goldD": "#e07d10", + "katex-goldE": "#a75a05", + "katex-redA": "#fca9a9", + "katex-redB": "#ff8482", + "katex-redC": "#f9685d", + "katex-redD": "#e84d39", + "katex-redE": "#bc2612", + "katex-maroonA": "#ffbde0", + "katex-maroonB": "#ff92c6", + "katex-maroonC": "#ed5fa6", + "katex-maroonD": "#ca337c", + "katex-maroonE": "#9e034e", + "katex-purpleA": "#ddd7ff", + "katex-purpleB": "#c6b9fc", + "katex-purpleC": "#aa87ff", + "katex-purpleD": "#7854ab", + "katex-purpleE": "#543b78", + "katex-mintA": "#f5f9e8", + "katex-mintB": "#edf2df", + "katex-mintC": "#e0e5cc", + "katex-grayA": "#f6f7f7", + "katex-grayB": "#f0f1f2", + "katex-grayC": "#e3e5e6", + "katex-grayD": "#d6d8da", + "katex-grayE": "#babec2", + "katex-grayF": "#888d93", + "katex-grayG": "#626569", + "katex-grayH": "#3b3e40", + "katex-grayI": "#21242c", + "katex-kaBlue": "#314453", + "katex-kaGreen": "#71B307", +}; + +/** + * Gets the CSS color of the current options object, accounting for the + * `colorMap`. + */ +Options.prototype.getColor = function() { + if (this.phantom) { + return "transparent"; + } else { + return colorMap[this.color] || this.color; + } +}; + +module.exports = Options; + +},{}],6:[function(require,module,exports){ +/** + * This is the ParseError class, which is the main error thrown by KaTeX + * functions when something has gone wrong. This is used to distinguish internal + * errors from errors in the expression that the user provided. + * + * If possible, a caller should provide a Token or ParseNode with information + * about where in the source string the problem occurred. + * + * @param {string} message The error message + * @param {(Token|ParseNode)=} token An object providing position information + */ +function ParseError(message, token) { + var error = "KaTeX parse error: " + message; + var start; + var end; + + if (token && token.lexer && token.start <= token.end) { + // If we have the input and a position, make the error a bit fancier + + // Get the input + var input = token.lexer.input; + + // Prepend some information + start = token.start; + end = token.end; + if (start === input.length) { + error += " at end of input: "; + } else { + error += " at position " + (start + 1) + ": "; + } + + // Underline token in question using combining underscores + var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); + + // Extract some context from the input and add it to the error + var left; + if (start > 15) { + left = "…" + input.slice(start - 15, start); + } else { + left = input.slice(0, start); + } + var right; + if (end + 15 < input.length) { + right = input.slice(end, end + 15) + "…"; + } else { + right = input.slice(end); + } + error += left + underlined + right; + } + + // Some hackery to make ParseError a prototype of Error + // See http://stackoverflow.com/a/8460753 + var self = new Error(error); + self.name = "ParseError"; + self.__proto__ = ParseError.prototype; + + self.position = start; + return self; +} + +// More hackery +ParseError.prototype.__proto__ = Error.prototype; + +module.exports = ParseError; + +},{}],7:[function(require,module,exports){ +/* eslint no-constant-condition:0 */ +var functions = require("./functions"); +var environments = require("./environments"); +var MacroExpander = require("./MacroExpander"); +var symbols = require("./symbols"); +var utils = require("./utils"); +var cjkRegex = require("./unicodeRegexes").cjkRegex; + +var parseData = require("./parseData"); +var ParseError = require("./ParseError"); + +/** + * This file contains the parser used to parse out a TeX expression from the + * input. Since TeX isn't context-free, standard parsers don't work particularly + * well. + * + * The strategy of this parser is as such: + * + * The main functions (the `.parse...` ones) take a position in the current + * parse string to parse tokens from. The lexer (found in Lexer.js, stored at + * this.lexer) also supports pulling out tokens at arbitrary places. When + * individual tokens are needed at a position, the lexer is called to pull out a + * token, which is then used. + * + * The parser has a property called "mode" indicating the mode that + * the parser is currently in. Currently it has to be one of "math" or + * "text", which denotes whether the current environment is a math-y + * one or a text-y one (e.g. inside \text). Currently, this serves to + * limit the functions which can be used in text mode. + * + * The main functions then return an object which contains the useful data that + * was parsed at its given point, and a new position at the end of the parsed + * data. The main functions can call each other and continue the parsing by + * using the returned position as a new starting point. + * + * There are also extra `.handle...` functions, which pull out some reused + * functionality into self-contained functions. + * + * The earlier functions return ParseNodes. + * The later functions (which are called deeper in the parse) sometimes return + * ParseFuncOrArgument, which contain a ParseNode as well as some data about + * whether the parsed object is a function which is missing some arguments, or a + * standalone object which can be used as an argument to another function. + */ + +/** + * Main Parser class + */ +function Parser(input, settings) { + // Create a new macro expander (gullet) and (indirectly via that) also a + // new lexer (mouth) for this parser (stomach, in the language of TeX) + this.gullet = new MacroExpander(input, settings.macros); + // Store the settings for use in parsing + this.settings = settings; +} + +var ParseNode = parseData.ParseNode; + +/** + * An initial function (without its arguments), or an argument to a function. + * The `result` argument should be a ParseNode. + */ +function ParseFuncOrArgument(result, isFunction, token) { + this.result = result; + // Is this a function (i.e. is it something defined in functions.js)? + this.isFunction = isFunction; + this.token = token; +} + +/** + * Checks a result to make sure it has the right type, and throws an + * appropriate error otherwise. + * + * @param {boolean=} consume whether to consume the expected token, + * defaults to true + */ +Parser.prototype.expect = function(text, consume) { + if (this.nextToken.text !== text) { + throw new ParseError( + "Expected '" + text + "', got '" + this.nextToken.text + "'", + this.nextToken + ); + } + if (consume !== false) { + this.consume(); + } +}; + +/** + * Considers the current look ahead token as consumed, + * and fetches the one after that as the new look ahead. + */ +Parser.prototype.consume = function() { + this.nextToken = this.gullet.get(this.mode === "math"); +}; + +Parser.prototype.switchMode = function(newMode) { + this.gullet.unget(this.nextToken); + this.mode = newMode; + this.consume(); +}; + +/** + * Main parsing function, which parses an entire input. + * + * @return {?Array.<ParseNode>} + */ +Parser.prototype.parse = function() { + // Try to parse the input + this.mode = "math"; + this.consume(); + var parse = this.parseInput(); + return parse; +}; + +/** + * Parses an entire input tree. + */ +Parser.prototype.parseInput = function() { + // Parse an expression + var expression = this.parseExpression(false); + // If we succeeded, make sure there's an EOF at the end + this.expect("EOF", false); + return expression; +}; + +var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; + +/** + * Parses an "expression", which is a list of atoms. + * + * @param {boolean} breakOnInfix Should the parsing stop when we hit infix + * nodes? This happens when functions have higher precendence + * than infix nodes in implicit parses. + * + * @param {?string} breakOnTokenText The text of the token that the expression + * should end with, or `null` if something else should end the + * expression. + * + * @return {ParseNode} + */ +Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) { + var body = []; + // Keep adding atoms to the body until we can't parse any more atoms (either + // we reached the end, a }, or a \right) + while (true) { + var lex = this.nextToken; + if (endOfExpression.indexOf(lex.text) !== -1) { + break; + } + if (breakOnTokenText && lex.text === breakOnTokenText) { + break; + } + if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { + break; + } + var atom = this.parseAtom(); + if (!atom) { + if (!this.settings.throwOnError && lex.text[0] === "\\") { + var errorNode = this.handleUnsupportedCmd(); + body.push(errorNode); + continue; + } + + break; + } + body.push(atom); + } + return this.handleInfixNodes(body); +}; + +/** + * Rewrites infix operators such as \over with corresponding commands such + * as \frac. + * + * There can only be one infix operator per group. If there's more than one + * then the expression is ambiguous. This can be resolved by adding {}. + * + * @returns {Array} + */ +Parser.prototype.handleInfixNodes = function(body) { + var overIndex = -1; + var funcName; + + for (var i = 0; i < body.length; i++) { + var node = body[i]; + if (node.type === "infix") { + if (overIndex !== -1) { + throw new ParseError( + "only one infix operator per group", + node.value.token); + } + overIndex = i; + funcName = node.value.replaceWith; + } + } + + if (overIndex !== -1) { + var numerNode; + var denomNode; + + var numerBody = body.slice(0, overIndex); + var denomBody = body.slice(overIndex + 1); + + if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { + numerNode = numerBody[0]; + } else { + numerNode = new ParseNode("ordgroup", numerBody, this.mode); + } + + if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { + denomNode = denomBody[0]; + } else { + denomNode = new ParseNode("ordgroup", denomBody, this.mode); + } + + var value = this.callFunction( + funcName, [numerNode, denomNode], null); + return [new ParseNode(value.type, value, this.mode)]; + } else { + return body; + } +}; + +// The greediness of a superscript or subscript +var SUPSUB_GREEDINESS = 1; + +/** + * Handle a subscript or superscript with nice errors. + */ +Parser.prototype.handleSupSubscript = function(name) { + var symbolToken = this.nextToken; + var symbol = symbolToken.text; + this.consume(); + var group = this.parseGroup(); + + if (!group) { + if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") { + return this.handleUnsupportedCmd(); + } else { + throw new ParseError( + "Expected group after '" + symbol + "'", + symbolToken + ); + } + } else if (group.isFunction) { + // ^ and _ have a greediness, so handle interactions with functions' + // greediness + var funcGreediness = functions[group.result].greediness; + if (funcGreediness > SUPSUB_GREEDINESS) { + return this.parseFunction(group); + } else { + throw new ParseError( + "Got function '" + group.result + "' with no arguments " + + "as " + name, symbolToken); + } + } else { + return group.result; + } +}; + +/** + * Converts the textual input of an unsupported command into a text node + * contained within a color node whose color is determined by errorColor + */ +Parser.prototype.handleUnsupportedCmd = function() { + var text = this.nextToken.text; + var textordArray = []; + + for (var i = 0; i < text.length; i++) { + textordArray.push(new ParseNode("textord", text[i], "text")); + } + + var textNode = new ParseNode( + "text", + { + body: textordArray, + type: "text", + }, + this.mode); + + var colorNode = new ParseNode( + "color", + { + color: this.settings.errorColor, + value: [textNode], + type: "color", + }, + this.mode); + + this.consume(); + return colorNode; +}; + +/** + * Parses a group with optional super/subscripts. + * + * @return {?ParseNode} + */ +Parser.prototype.parseAtom = function() { + // The body of an atom is an implicit group, so that things like + // \left(x\right)^2 work correctly. + var base = this.parseImplicitGroup(); + + // In text mode, we don't have superscripts or subscripts + if (this.mode === "text") { + return base; + } + + // Note that base may be empty (i.e. null) at this point. + + var superscript; + var subscript; + while (true) { + // Lex the first token + var lex = this.nextToken; + + if (lex.text === "\\limits" || lex.text === "\\nolimits") { + // We got a limit control + if (!base || base.type !== "op") { + throw new ParseError( + "Limit controls must follow a math operator", + lex); + } else { + var limits = lex.text === "\\limits"; + base.value.limits = limits; + base.value.alwaysHandleSupSub = true; + } + this.consume(); + } else if (lex.text === "^") { + // We got a superscript start + if (superscript) { + throw new ParseError("Double superscript", lex); + } + superscript = this.handleSupSubscript("superscript"); + } else if (lex.text === "_") { + // We got a subscript start + if (subscript) { + throw new ParseError("Double subscript", lex); + } + subscript = this.handleSupSubscript("subscript"); + } else if (lex.text === "'") { + // We got a prime + var prime = new ParseNode("textord", "\\prime", this.mode); + + // Many primes can be grouped together, so we handle this here + var primes = [prime]; + this.consume(); + // Keep lexing tokens until we get something that's not a prime + while (this.nextToken.text === "'") { + // For each one, add another prime to the list + primes.push(prime); + this.consume(); + } + // Put them into an ordgroup as the superscript + superscript = new ParseNode("ordgroup", primes, this.mode); + } else { + // If it wasn't ^, _, or ', stop parsing super/subscripts + break; + } + } + + if (superscript || subscript) { + // If we got either a superscript or subscript, create a supsub + return new ParseNode("supsub", { + base: base, + sup: superscript, + sub: subscript, + }, this.mode); + } else { + // Otherwise return the original body + return base; + } +}; + +// A list of the size-changing functions, for use in parseImplicitGroup +var sizeFuncs = [ + "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", + "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", +]; + +// A list of the style-changing functions, for use in parseImplicitGroup +var styleFuncs = [ + "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle", +]; + +/** + * Parses an implicit group, which is a group that starts at the end of a + * specified, and ends right before a higher explicit group ends, or at EOL. It + * is used for functions that appear to affect the current style, like \Large or + * \textrm, where instead of keeping a style we just pretend that there is an + * implicit grouping after it until the end of the group. E.g. + * small text {\Large large text} small text again + * It is also used for \left and \right to get the correct grouping. + * + * @return {?ParseNode} + */ +Parser.prototype.parseImplicitGroup = function() { + var start = this.parseSymbol(); + + if (start == null) { + // If we didn't get anything we handle, fall back to parseFunction + return this.parseFunction(); + } + + var func = start.result; + var body; + + if (func === "\\left") { + // If we see a left: + // Parse the entire left function (including the delimiter) + var left = this.parseFunction(start); + // Parse out the implicit body + body = this.parseExpression(false); + // Check the next token + this.expect("\\right", false); + var right = this.parseFunction(); + return new ParseNode("leftright", { + body: body, + left: left.value.value, + right: right.value.value, + }, this.mode); + } else if (func === "\\begin") { + // begin...end is similar to left...right + var begin = this.parseFunction(start); + var envName = begin.value.name; + if (!environments.hasOwnProperty(envName)) { + throw new ParseError( + "No such environment: " + envName, begin.value.nameGroup); + } + // Build the environment object. Arguments and other information will + // be made available to the begin and end methods using properties. + var env = environments[envName]; + var args = this.parseArguments("\\begin{" + envName + "}", env); + var context = { + mode: this.mode, + envName: envName, + parser: this, + positions: args.pop(), + }; + var result = env.handler(context, args); + this.expect("\\end", false); + var endNameToken = this.nextToken; + var end = this.parseFunction(); + if (end.value.name !== envName) { + throw new ParseError( + "Mismatch: \\begin{" + envName + "} matched " + + "by \\end{" + end.value.name + "}", + endNameToken); + } + result.position = end.position; + return result; + } else if (utils.contains(sizeFuncs, func)) { + // If we see a sizing function, parse out the implict body + body = this.parseExpression(false); + return new ParseNode("sizing", { + // Figure out what size to use based on the list of functions above + size: "size" + (utils.indexOf(sizeFuncs, func) + 1), + value: body, + }, this.mode); + } else if (utils.contains(styleFuncs, func)) { + // If we see a styling function, parse out the implict body + body = this.parseExpression(true); + return new ParseNode("styling", { + // Figure out what style to use by pulling out the style from + // the function name + style: func.slice(1, func.length - 5), + value: body, + }, this.mode); + } else { + // Defer to parseFunction if it's not a function we handle + return this.parseFunction(start); + } +}; + +/** + * Parses an entire function, including its base and all of its arguments. + * The base might either have been parsed already, in which case + * it is provided as an argument, or it's the next group in the input. + * + * @param {ParseFuncOrArgument=} baseGroup optional as described above + * @return {?ParseNode} + */ +Parser.prototype.parseFunction = function(baseGroup) { + if (!baseGroup) { + baseGroup = this.parseGroup(); + } + + if (baseGroup) { + if (baseGroup.isFunction) { + var func = baseGroup.result; + var funcData = functions[func]; + if (this.mode === "text" && !funcData.allowedInText) { + throw new ParseError( + "Can't use function '" + func + "' in text mode", + baseGroup.token); + } + + var args = this.parseArguments(func, funcData); + var token = baseGroup.token; + var result = this.callFunction(func, args, args.pop(), token); + return new ParseNode(result.type, result, this.mode); + } else { + return baseGroup.result; + } + } else { + return null; + } +}; + +/** + * Call a function handler with a suitable context and arguments. + */ +Parser.prototype.callFunction = function(name, args, positions, token) { + var context = { + funcName: name, + parser: this, + positions: positions, + token: token, + }; + return functions[name].handler(context, args); +}; + +/** + * Parses the arguments of a function or environment + * + * @param {string} func "\name" or "\begin{name}" + * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData + * @return the array of arguments, with the list of positions as last element + */ +Parser.prototype.parseArguments = function(func, funcData) { + var totalArgs = funcData.numArgs + funcData.numOptionalArgs; + if (totalArgs === 0) { + return [[this.pos]]; + } + + var baseGreediness = funcData.greediness; + var positions = [this.pos]; + var args = []; + + for (var i = 0; i < totalArgs; i++) { + var nextToken = this.nextToken; + var argType = funcData.argTypes && funcData.argTypes[i]; + var arg; + if (i < funcData.numOptionalArgs) { + if (argType) { + arg = this.parseGroupOfType(argType, true); + } else { + arg = this.parseGroup(true); + } + if (!arg) { + args.push(null); + positions.push(this.pos); + continue; + } + } else { + if (argType) { + arg = this.parseGroupOfType(argType); + } else { + arg = this.parseGroup(); + } + if (!arg) { + if (!this.settings.throwOnError && + this.nextToken.text[0] === "\\") { + arg = new ParseFuncOrArgument( + this.handleUnsupportedCmd(this.nextToken.text), + false); + } else { + throw new ParseError( + "Expected group after '" + func + "'", nextToken); + } + } + } + var argNode; + if (arg.isFunction) { + var argGreediness = + functions[arg.result].greediness; + if (argGreediness > baseGreediness) { + argNode = this.parseFunction(arg); + } else { + throw new ParseError( + "Got function '" + arg.result + "' as " + + "argument to '" + func + "'", nextToken); + } + } else { + argNode = arg.result; + } + args.push(argNode); + positions.push(this.pos); + } + + args.push(positions); + + return args; +}; + + +/** + * Parses a group when the mode is changing. + * + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseGroupOfType = function(innerMode, optional) { + var outerMode = this.mode; + // Handle `original` argTypes + if (innerMode === "original") { + innerMode = outerMode; + } + + if (innerMode === "color") { + return this.parseColorGroup(optional); + } + if (innerMode === "size") { + return this.parseSizeGroup(optional); + } + + this.switchMode(innerMode); + if (innerMode === "text") { + // text mode is special because it should ignore the whitespace before + // it + while (this.nextToken.text === " ") { + this.consume(); + } + } + // By the time we get here, innerMode is one of "text" or "math". + // We switch the mode of the parser, recurse, then restore the old mode. + var res = this.parseGroup(optional); + this.switchMode(outerMode); + return res; +}; + +/** + * Parses a group, essentially returning the string formed by the + * brace-enclosed tokens plus some position information. + * + * @param {string} modeName Used to describe the mode in error messages + * @param {boolean=} optional Whether the group is optional or required + */ +Parser.prototype.parseStringGroup = function(modeName, optional) { + if (optional && this.nextToken.text !== "[") { + return null; + } + var outerMode = this.mode; + this.mode = "text"; + this.expect(optional ? "[" : "{"); + var str = ""; + var firstToken = this.nextToken; + var lastToken = firstToken; + while (this.nextToken.text !== (optional ? "]" : "}")) { + if (this.nextToken.text === "EOF") { + throw new ParseError( + "Unexpected end of input in " + modeName, + firstToken.range(this.nextToken, str)); + } + lastToken = this.nextToken; + str += lastToken.text; + this.consume(); + } + this.mode = outerMode; + this.expect(optional ? "]" : "}"); + return firstToken.range(lastToken, str); +}; + +/** + * Parses a color description. + */ +Parser.prototype.parseColorGroup = function(optional) { + var res = this.parseStringGroup("color", optional); + if (!res) { + return null; + } + var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text); + if (!match) { + throw new ParseError("Invalid color: '" + res.text + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", match[0], this.mode), + false); +}; + +/** + * Parses a size specification, consisting of magnitude and unit. + */ +Parser.prototype.parseSizeGroup = function(optional) { + var res = this.parseStringGroup("size", optional); + if (!res) { + return null; + } + var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); + if (!match) { + throw new ParseError("Invalid size: '" + res.text + "'", res); + } + var data = { + number: +(match[1] + match[2]), // sign + magnitude, cast to number + unit: match[3], + }; + if (data.unit !== "em" && data.unit !== "ex") { + throw new ParseError("Invalid unit: '" + data.unit + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", data, this.mode), + false); +}; + +/** + * If the argument is false or absent, this parses an ordinary group, + * which is either a single nucleus (like "x") or an expression + * in braces (like "{x+y}"). + * If the argument is true, it parses either a bracket-delimited expression + * (like "[x+y]") or returns null to indicate the absence of a + * bracket-enclosed group. + * + * @param {boolean=} optional Whether the group is optional or required + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseGroup = function(optional) { + var firstToken = this.nextToken; + // Try to parse an open brace + if (this.nextToken.text === (optional ? "[" : "{")) { + // If we get a brace, parse an expression + this.consume(); + var expression = this.parseExpression(false, optional ? "]" : null); + var lastToken = this.nextToken; + // Make sure we get a close brace + this.expect(optional ? "]" : "}"); + if (this.mode === "text") { + this.formLigatures(expression); + } + return new ParseFuncOrArgument( + new ParseNode("ordgroup", expression, this.mode, + firstToken, lastToken), + false); + } else { + // Otherwise, just return a nucleus, or nothing for an optional group + return optional ? null : this.parseSymbol(); + } +}; + +/** + * Form ligature-like combinations of characters for text mode. + * This includes inputs like "--", "---", "``" and "''". + * The result will simply replace multiple textord nodes with a single + * character in each value by a single textord node having multiple + * characters in its value. The representation is still ASCII source. + * + * @param {Array.<ParseNode>} group the nodes of this group, + * list will be moified in place + */ +Parser.prototype.formLigatures = function(group) { + var i; + var n = group.length - 1; + for (i = 0; i < n; ++i) { + var a = group[i]; + var v = a.value; + if (v === "-" && group[i + 1].value === "-") { + if (i + 1 < n && group[i + 2].value === "-") { + group.splice(i, 3, new ParseNode( + "textord", "---", "text", a, group[i + 2])); + n -= 2; + } else { + group.splice(i, 2, new ParseNode( + "textord", "--", "text", a, group[i + 1])); + n -= 1; + } + } + if ((v === "'" || v === "`") && group[i + 1].value === v) { + group.splice(i, 2, new ParseNode( + "textord", v + v, "text", a, group[i + 1])); + n -= 1; + } + } +}; + +/** + * Parse a single symbol out of the string. Here, we handle both the functions + * we have defined, as well as the single character symbols + * + * @return {?ParseFuncOrArgument} + */ +Parser.prototype.parseSymbol = function() { + var nucleus = this.nextToken; + + if (functions[nucleus.text]) { + this.consume(); + // If there exists a function with this name, we return the function and + // say that it is a function. + return new ParseFuncOrArgument( + nucleus.text, + true, nucleus); + } else if (symbols[this.mode][nucleus.text]) { + this.consume(); + // Otherwise if this is a no-argument function, find the type it + // corresponds to in the symbols map + return new ParseFuncOrArgument( + new ParseNode(symbols[this.mode][nucleus.text].group, + nucleus.text, this.mode, nucleus), + false, nucleus); + } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) { + this.consume(); + return new ParseFuncOrArgument( + new ParseNode("textord", nucleus.text, this.mode, nucleus), + false, nucleus); + } else { + return null; + } +}; + +Parser.prototype.ParseNode = ParseNode; + +module.exports = Parser; + +},{"./MacroExpander":4,"./ParseError":6,"./environments":16,"./functions":19,"./parseData":21,"./symbols":23,"./unicodeRegexes":24,"./utils":25}],8:[function(require,module,exports){ +/** + * This is a module for storing settings passed into KaTeX. It correctly handles + * default settings. + */ + +/** + * Helper function for getting a default value if the value is undefined + */ +function get(option, defaultValue) { + return option === undefined ? defaultValue : option; +} + +/** + * The main Settings object + * + * The current options stored are: + * - displayMode: Whether the expression should be typeset by default in + * textstyle or displaystyle (default false) + */ +function Settings(options) { + // allow null options + options = options || {}; + this.displayMode = get(options.displayMode, false); + this.throwOnError = get(options.throwOnError, true); + this.errorColor = get(options.errorColor, "#cc0000"); + this.macros = options.macros || {}; +} + +module.exports = Settings; + +},{}],9:[function(require,module,exports){ +/** + * This file contains information and classes for the various kinds of styles + * used in TeX. It provides a generic `Style` class, which holds information + * about a specific style. It then provides instances of all the different kinds + * of styles possible, and provides functions to move between them and get + * information about them. + */ + +/** + * The main style class. Contains a unique id for the style, a size (which is + * the same for cramped and uncramped version of a style), a cramped flag, and a + * size multiplier, which gives the size difference between a style and + * textstyle. + */ +function Style(id, size, multiplier, cramped) { + this.id = id; + this.size = size; + this.cramped = cramped; + this.sizeMultiplier = multiplier; +} + +/** + * Get the style of a superscript given a base in the current style. + */ +Style.prototype.sup = function() { + return styles[sup[this.id]]; +}; + +/** + * Get the style of a subscript given a base in the current style. + */ +Style.prototype.sub = function() { + return styles[sub[this.id]]; +}; + +/** + * Get the style of a fraction numerator given the fraction in the current + * style. + */ +Style.prototype.fracNum = function() { + return styles[fracNum[this.id]]; +}; + +/** + * Get the style of a fraction denominator given the fraction in the current + * style. + */ +Style.prototype.fracDen = function() { + return styles[fracDen[this.id]]; +}; + +/** + * Get the cramped version of a style (in particular, cramping a cramped style + * doesn't change the style). + */ +Style.prototype.cramp = function() { + return styles[cramp[this.id]]; +}; + +/** + * HTML class name, like "displaystyle cramped" + */ +Style.prototype.cls = function() { + return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped"); +}; + +/** + * HTML Reset class name, like "reset-textstyle" + */ +Style.prototype.reset = function() { + return resetNames[this.size]; +}; + +// IDs of the different styles +var D = 0; +var Dc = 1; +var T = 2; +var Tc = 3; +var S = 4; +var Sc = 5; +var SS = 6; +var SSc = 7; + +// String names for the different sizes +var sizeNames = [ + "displaystyle textstyle", + "textstyle", + "scriptstyle", + "scriptscriptstyle", +]; + +// Reset names for the different sizes +var resetNames = [ + "reset-textstyle", + "reset-textstyle", + "reset-scriptstyle", + "reset-scriptscriptstyle", +]; + +// Instances of the different styles +var styles = [ + new Style(D, 0, 1.0, false), + new Style(Dc, 0, 1.0, true), + new Style(T, 1, 1.0, false), + new Style(Tc, 1, 1.0, true), + new Style(S, 2, 0.7, false), + new Style(Sc, 2, 0.7, true), + new Style(SS, 3, 0.5, false), + new Style(SSc, 3, 0.5, true), +]; + +// Lookup tables for switching from one style to another +var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; +var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; +var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; +var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; +var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; + +// We only export some of the styles. Also, we don't export the `Style` class so +// no more styles can be generated. +module.exports = { + DISPLAY: styles[D], + TEXT: styles[T], + SCRIPT: styles[S], + SCRIPTSCRIPT: styles[SS], +}; + +},{}],10:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This module contains general functions that can be used for building + * different kinds of domTree nodes in a consistent manner. + */ + +var domTree = require("./domTree"); +var fontMetrics = require("./fontMetrics"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var greekCapitals = [ + "\\Gamma", + "\\Delta", + "\\Theta", + "\\Lambda", + "\\Xi", + "\\Pi", + "\\Sigma", + "\\Upsilon", + "\\Phi", + "\\Psi", + "\\Omega", +]; + +// The following have to be loaded from Main-Italic font, using class mainit +var mainitLetters = [ + "\u0131", // dotless i, \imath + "\u0237", // dotless j, \jmath + "\u00a3", // \pounds +]; + +/** + * Makes a symbolNode after translation via the list of symbols in symbols.js. + * Correctly pulls out metrics for the character, and optionally takes a list of + * classes to be attached to the node. + */ +var makeSymbol = function(value, style, mode, color, classes) { + // Replace the value with its replaced value from symbol.js + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var metrics = fontMetrics.getCharacterMetrics(value, style); + + var symbolNode; + if (metrics) { + symbolNode = new domTree.symbolNode( + value, metrics.height, metrics.depth, metrics.italic, metrics.skew, + classes); + } else { + // TODO(emily): Figure out a good way to only print this in development + typeof console !== "undefined" && console.warn( + "No character metrics for '" + value + "' in style '" + + style + "'"); + symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes); + } + + if (color) { + symbolNode.style.color = color; + } + + return symbolNode; +}; + +/** + * Makes a symbol in Main-Regular or AMS-Regular. + * Used for rel, bin, open, close, inner, and punct. + */ +var mathsym = function(value, mode, color, classes) { + // Decide what font to render the symbol in by its entry in the symbols + // table. + // Have a special case for when the value = \ because the \ is used as a + // textord in unsupported command errors but cannot be parsed as a regular + // text ordinal and is therefore not present as a symbol in the symbols + // table for text + if (value === "\\" || symbols[mode][value].font === "main") { + return makeSymbol(value, "Main-Regular", mode, color, classes); + } else { + return makeSymbol( + value, "AMS-Regular", mode, color, classes.concat(["amsrm"])); + } +}; + +/** + * Makes a symbol in the default font for mathords and textords. + */ +var mathDefault = function(value, mode, color, classes, type) { + if (type === "mathord") { + return mathit(value, mode, color, classes); + } else if (type === "textord") { + return makeSymbol( + value, "Main-Regular", mode, color, classes.concat(["mathrm"])); + } else { + throw new Error("unexpected type: " + type + " in mathDefault"); + } +}; + +/** + * Makes a symbol in the italic math font. + */ +var mathit = function(value, mode, color, classes) { + if (/[0-9]/.test(value.charAt(0)) || + // glyphs for \imath and \jmath do not exist in Math-Italic so we + // need to use Main-Italic instead + utils.contains(mainitLetters, value) || + utils.contains(greekCapitals, value)) { + return makeSymbol( + value, "Main-Italic", mode, color, classes.concat(["mainit"])); + } else { + return makeSymbol( + value, "Math-Italic", mode, color, classes.concat(["mathit"])); + } +}; + +/** + * Makes either a mathord or textord in the correct font and color. + */ +var makeOrd = function(group, options, type) { + var mode = group.mode; + var value = group.value; + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var classes = ["mord"]; + var color = options.getColor(); + + var font = options.font; + if (font) { + if (font === "mathit" || utils.contains(mainitLetters, value)) { + return mathit(value, mode, color, classes); + } else { + var fontName = fontMap[font].fontName; + if (fontMetrics.getCharacterMetrics(value, fontName)) { + return makeSymbol( + value, fontName, mode, color, classes.concat([font])); + } else { + return mathDefault(value, mode, color, classes, type); + } + } + } else { + return mathDefault(value, mode, color, classes, type); + } +}; + +/** + * Calculate the height, depth, and maxFontSize of an element based on its + * children. + */ +var sizeElementFromChildren = function(elem) { + var height = 0; + var depth = 0; + var maxFontSize = 0; + + if (elem.children) { + for (var i = 0; i < elem.children.length; i++) { + if (elem.children[i].height > height) { + height = elem.children[i].height; + } + if (elem.children[i].depth > depth) { + depth = elem.children[i].depth; + } + if (elem.children[i].maxFontSize > maxFontSize) { + maxFontSize = elem.children[i].maxFontSize; + } + } + } + + elem.height = height; + elem.depth = depth; + elem.maxFontSize = maxFontSize; +}; + +/** + * Makes a span with the given list of classes, list of children, and color. + */ +var makeSpan = function(classes, children, color) { + var span = new domTree.span(classes, children); + + sizeElementFromChildren(span); + + if (color) { + span.style.color = color; + } + + return span; +}; + +/** + * Makes a document fragment with the given list of children. + */ +var makeFragment = function(children) { + var fragment = new domTree.documentFragment(children); + + sizeElementFromChildren(fragment); + + return fragment; +}; + +/** + * Makes an element placed in each of the vlist elements to ensure that each + * element has the same max font size. To do this, we create a zero-width space + * with the correct font size. + */ +var makeFontSizer = function(options, fontSize) { + var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]); + fontSizeInner.style.fontSize = + (fontSize / options.style.sizeMultiplier) + "em"; + + var fontSizer = makeSpan( + ["fontsize-ensurer", "reset-" + options.size, "size5"], + [fontSizeInner]); + + return fontSizer; +}; + +/** + * Makes a vertical list by stacking elements and kerns on top of each other. + * Allows for many different ways of specifying the positioning method. + * + * Arguments: + * - children: A list of child or kern nodes to be stacked on top of each other + * (i.e. the first element will be at the bottom, and the last at + * the top). Element nodes are specified as + * {type: "elem", elem: node} + * while kern nodes are specified as + * {type: "kern", size: size} + * - positionType: The method by which the vlist should be positioned. Valid + * values are: + * - "individualShift": The children list only contains elem + * nodes, and each node contains an extra + * "shift" value of how much it should be + * shifted (note that shifting is always + * moving downwards). positionData is + * ignored. + * - "top": The positionData specifies the topmost point of + * the vlist (note this is expected to be a height, + * so positive values move up) + * - "bottom": The positionData specifies the bottommost point + * of the vlist (note this is expected to be a + * depth, so positive values move down + * - "shift": The vlist will be positioned such that its + * baseline is positionData away from the baseline + * of the first child. Positive values move + * downwards. + * - "firstBaseline": The vlist will be positioned such that + * its baseline is aligned with the + * baseline of the first child. + * positionData is ignored. (this is + * equivalent to "shift" with + * positionData=0) + * - positionData: Data used in different ways depending on positionType + * - options: An Options object + * + */ +var makeVList = function(children, positionType, positionData, options) { + var depth; + var currPos; + var i; + if (positionType === "individualShift") { + var oldChildren = children; + children = [oldChildren[0]]; + + // Add in kerns to the list of children to get each element to be + // shifted to the correct specified shift + depth = -oldChildren[0].shift - oldChildren[0].elem.depth; + currPos = depth; + for (i = 1; i < oldChildren.length; i++) { + var diff = -oldChildren[i].shift - currPos - + oldChildren[i].elem.depth; + var size = diff - + (oldChildren[i - 1].elem.height + + oldChildren[i - 1].elem.depth); + + currPos = currPos + diff; + + children.push({type: "kern", size: size}); + children.push(oldChildren[i]); + } + } else if (positionType === "top") { + // We always start at the bottom, so calculate the bottom by adding up + // all the sizes + var bottom = positionData; + for (i = 0; i < children.length; i++) { + if (children[i].type === "kern") { + bottom -= children[i].size; + } else { + bottom -= children[i].elem.height + children[i].elem.depth; + } + } + depth = bottom; + } else if (positionType === "bottom") { + depth = -positionData; + } else if (positionType === "shift") { + depth = -children[0].elem.depth - positionData; + } else if (positionType === "firstBaseline") { + depth = -children[0].elem.depth; + } else { + depth = 0; + } + + // Make the fontSizer + var maxFontSize = 0; + for (i = 0; i < children.length; i++) { + if (children[i].type === "elem") { + maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize); + } + } + var fontSizer = makeFontSizer(options, maxFontSize); + + // Create a new list of actual children at the correct offsets + var realChildren = []; + currPos = depth; + for (i = 0; i < children.length; i++) { + if (children[i].type === "kern") { + currPos += children[i].size; + } else { + var child = children[i].elem; + + var shift = -child.depth - currPos; + currPos += child.height + child.depth; + + var childWrap = makeSpan([], [fontSizer, child]); + childWrap.height -= shift; + childWrap.depth += shift; + childWrap.style.top = shift + "em"; + + realChildren.push(childWrap); + } + } + + // Add in an element at the end with no offset to fix the calculation of + // baselines in some browsers (namely IE, sometimes safari) + var baselineFix = makeSpan( + ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]); + realChildren.push(baselineFix); + + var vlist = makeSpan(["vlist"], realChildren); + // Fix the final height and depth, in case there were kerns at the ends + // since the makeSpan calculation won't take that in to account. + vlist.height = Math.max(currPos, vlist.height); + vlist.depth = Math.max(-depth, vlist.depth); + return vlist; +}; + +// A table of size -> font size for the different sizing functions +var sizingMultiplier = { + size1: 0.5, + size2: 0.7, + size3: 0.8, + size4: 0.9, + size5: 1.0, + size6: 1.2, + size7: 1.44, + size8: 1.73, + size9: 2.07, + size10: 2.49, +}; + +// A map of spacing functions to their attributes, like size and corresponding +// CSS class +var spacingFunctions = { + "\\qquad": { + size: "2em", + className: "qquad", + }, + "\\quad": { + size: "1em", + className: "quad", + }, + "\\enspace": { + size: "0.5em", + className: "enspace", + }, + "\\;": { + size: "0.277778em", + className: "thickspace", + }, + "\\:": { + size: "0.22222em", + className: "mediumspace", + }, + "\\,": { + size: "0.16667em", + className: "thinspace", + }, + "\\!": { + size: "-0.16667em", + className: "negativethinspace", + }, +}; + +/** + * Maps TeX font commands to objects containing: + * - variant: string used for "mathvariant" attribute in buildMathML.js + * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics + */ +// A map between tex font commands an MathML mathvariant attribute values +var fontMap = { + // styles + "mathbf": { + variant: "bold", + fontName: "Main-Bold", + }, + "mathrm": { + variant: "normal", + fontName: "Main-Regular", + }, + + // "mathit" is missing because it requires the use of two fonts: Main-Italic + // and Math-Italic. This is handled by a special case in makeOrd which ends + // up calling mathit. + + // families + "mathbb": { + variant: "double-struck", + fontName: "AMS-Regular", + }, + "mathcal": { + variant: "script", + fontName: "Caligraphic-Regular", + }, + "mathfrak": { + variant: "fraktur", + fontName: "Fraktur-Regular", + }, + "mathscr": { + variant: "script", + fontName: "Script-Regular", + }, + "mathsf": { + variant: "sans-serif", + fontName: "SansSerif-Regular", + }, + "mathtt": { + variant: "monospace", + fontName: "Typewriter-Regular", + }, +}; + +module.exports = { + fontMap: fontMap, + makeSymbol: makeSymbol, + mathsym: mathsym, + makeSpan: makeSpan, + makeFragment: makeFragment, + makeVList: makeVList, + makeOrd: makeOrd, + sizingMultiplier: sizingMultiplier, + spacingFunctions: spacingFunctions, +}; + +},{"./domTree":15,"./fontMetrics":17,"./symbols":23,"./utils":25}],11:[function(require,module,exports){ +/* eslint no-console:0 */ +/** + * This file does the main work of building a domTree structure from a parse + * tree. The entry point is the `buildHTML` function, which takes a parse tree. + * Then, the buildExpression, buildGroup, and various groupTypes functions are + * called, to produce a final HTML tree. + */ + +var ParseError = require("./ParseError"); +var Style = require("./Style"); + +var buildCommon = require("./buildCommon"); +var delimiter = require("./delimiter"); +var domTree = require("./domTree"); +var fontMetrics = require("./fontMetrics"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; + +/** + * Take a list of nodes, build them in order, and return a list of the built + * nodes. This function handles the `prev` node correctly, and passes the + * previous element from the list as the prev of the next element. + */ +var buildExpression = function(expression, options, prev) { + var groups = []; + for (var i = 0; i < expression.length; i++) { + var group = expression[i]; + groups.push(buildGroup(group, options, prev)); + prev = group; + } + return groups; +}; + +// List of types used by getTypeOfGroup, +// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types +var groupToType = { + mathord: "mord", + textord: "mord", + bin: "mbin", + rel: "mrel", + text: "mord", + open: "mopen", + close: "mclose", + inner: "minner", + genfrac: "mord", + array: "mord", + spacing: "mord", + punct: "mpunct", + ordgroup: "mord", + op: "mop", + katex: "mord", + overline: "mord", + underline: "mord", + rule: "mord", + leftright: "minner", + sqrt: "mord", + accent: "mord", +}; + +/** + * Gets the final math type of an expression, given its group type. This type is + * used to determine spacing between elements, and affects bin elements by + * causing them to change depending on what types are around them. This type + * must be attached to the outermost node of an element as a CSS class so that + * spacing with its surrounding elements works correctly. + * + * Some elements can be mapped one-to-one from group type to math type, and + * those are listed in the `groupToType` table. + * + * Others (usually elements that wrap around other elements) often have + * recursive definitions, and thus call `getTypeOfGroup` on their inner + * elements. + */ +var getTypeOfGroup = function(group) { + if (group == null) { + // Like when typesetting $^3$ + return groupToType.mathord; + } else if (group.type === "supsub") { + return getTypeOfGroup(group.value.base); + } else if (group.type === "llap" || group.type === "rlap") { + return getTypeOfGroup(group.value); + } else if (group.type === "color") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "sizing") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "styling") { + return getTypeOfGroup(group.value.value); + } else if (group.type === "delimsizing") { + return groupToType[group.value.delimType]; + } else { + return groupToType[group.type]; + } +}; + +/** + * Sometimes, groups perform special rules when they have superscripts or + * subscripts attached to them. This function lets the `supsub` group know that + * its inner element should handle the superscripts and subscripts instead of + * handling them itself. + */ +var shouldHandleSupSub = function(group, options) { + if (!group) { + return false; + } else if (group.type === "op") { + // Operators handle supsubs differently when they have limits + // (e.g. `\displaystyle\sum_2^3`) + return group.value.limits && + (options.style.size === Style.DISPLAY.size || + group.value.alwaysHandleSupSub); + } else if (group.type === "accent") { + return isCharacterBox(group.value.base); + } else { + return null; + } +}; + +/** + * Sometimes we want to pull out the innermost element of a group. In most + * cases, this will just be the group itself, but when ordgroups and colors have + * a single element, we want to pull that out. + */ +var getBaseElem = function(group) { + if (!group) { + return false; + } else if (group.type === "ordgroup") { + if (group.value.length === 1) { + return getBaseElem(group.value[0]); + } else { + return group; + } + } else if (group.type === "color") { + if (group.value.value.length === 1) { + return getBaseElem(group.value.value[0]); + } else { + return group; + } + } else if (group.type === "font") { + return getBaseElem(group.value.body); + } else { + return group; + } +}; + +/** + * TeXbook algorithms often reference "character boxes", which are simply groups + * with a single character in them. To decide if something is a character box, + * we find its innermost group, and see if it is a single character. + */ +var isCharacterBox = function(group) { + var baseElem = getBaseElem(group); + + // These are all they types of groups which hold single characters + return baseElem.type === "mathord" || + baseElem.type === "textord" || + baseElem.type === "bin" || + baseElem.type === "rel" || + baseElem.type === "inner" || + baseElem.type === "open" || + baseElem.type === "close" || + baseElem.type === "punct"; +}; + +var makeNullDelimiter = function(options) { + return makeSpan([ + "sizing", "reset-" + options.size, "size5", + options.style.reset(), Style.TEXT.cls(), + "nulldelimiter", + ]); +}; + +/** + * This is a map of group types to the function used to handle that type. + * Simpler types come at the beginning, while complicated types come afterwards. + */ +var groupTypes = {}; + +groupTypes.mathord = function(group, options, prev) { + return buildCommon.makeOrd(group, options, "mathord"); +}; + +groupTypes.textord = function(group, options, prev) { + return buildCommon.makeOrd(group, options, "textord"); +}; + +groupTypes.bin = function(group, options, prev) { + var className = "mbin"; + // Pull out the most recent element. Do some special handling to find + // things at the end of a \color group. Note that we don't use the same + // logic for ordgroups (which count as ords). + var prevAtom = prev; + while (prevAtom && prevAtom.type === "color") { + var atoms = prevAtom.value.value; + prevAtom = atoms[atoms.length - 1]; + } + // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19. + // Here, we determine whether the bin should turn into an ord. We + // currently only apply Rule 5. + if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"], + getTypeOfGroup(prevAtom))) { + group.type = "textord"; + className = "mord"; + } + + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), [className]); +}; + +groupTypes.rel = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mrel"]); +}; + +groupTypes.open = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mopen"]); +}; + +groupTypes.close = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mclose"]); +}; + +groupTypes.inner = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["minner"]); +}; + +groupTypes.punct = function(group, options, prev) { + return buildCommon.mathsym( + group.value, group.mode, options.getColor(), ["mpunct"]); +}; + +groupTypes.ordgroup = function(group, options, prev) { + return makeSpan( + ["mord", options.style.cls()], + buildExpression(group.value, options.reset()) + ); +}; + +groupTypes.text = function(group, options, prev) { + return makeSpan(["text", "mord", options.style.cls()], + buildExpression(group.value.body, options.reset())); +}; + +groupTypes.color = function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withColor(group.value.color), + prev + ); + + // \color isn't supposed to affect the type of the elements it contains. + // To accomplish this, we wrap the results in a fragment, so the inner + // elements will be able to directly interact with their neighbors. For + // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` + return new buildCommon.makeFragment(elements); +}; + +groupTypes.supsub = function(group, options, prev) { + // Superscript and subscripts are handled in the TeXbook on page + // 445-446, rules 18(a-f). + + // Here is where we defer to the inner group if it should handle + // superscripts and subscripts itself. + if (shouldHandleSupSub(group.value.base, options)) { + return groupTypes[group.value.base.type](group, options, prev); + } + + var base = buildGroup(group.value.base, options.reset()); + var supmid; + var submid; + var sup; + var sub; + + if (group.value.sup) { + sup = buildGroup(group.value.sup, + options.withStyle(options.style.sup())); + supmid = makeSpan( + [options.style.reset(), options.style.sup().cls()], [sup]); + } + + if (group.value.sub) { + sub = buildGroup(group.value.sub, + options.withStyle(options.style.sub())); + submid = makeSpan( + [options.style.reset(), options.style.sub().cls()], [sub]); + } + + // Rule 18a + var supShift; + var subShift; + if (isCharacterBox(group.value.base)) { + supShift = 0; + subShift = 0; + } else { + supShift = base.height - fontMetrics.metrics.supDrop; + subShift = base.depth + fontMetrics.metrics.subDrop; + } + + // Rule 18c + var minSupShift; + if (options.style === Style.DISPLAY) { + minSupShift = fontMetrics.metrics.sup1; + } else if (options.style.cramped) { + minSupShift = fontMetrics.metrics.sup3; + } else { + minSupShift = fontMetrics.metrics.sup2; + } + + // scriptspace is a font-size-independent size, so scale it + // appropriately + var multiplier = Style.TEXT.sizeMultiplier * + options.style.sizeMultiplier; + var scriptspace = + (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em"; + + var supsub; + if (!group.value.sup) { + // Rule 18b + subShift = Math.max( + subShift, fontMetrics.metrics.sub1, + sub.height - 0.8 * fontMetrics.metrics.xHeight); + + supsub = buildCommon.makeVList([ + {type: "elem", elem: submid}, + ], "shift", subShift, options); + + supsub.children[0].style.marginRight = scriptspace; + + // Subscripts shouldn't be shifted by the base's italic correction. + // Account for that by shifting the subscript back the appropriate + // amount. Note we only do this when the base is a single symbol. + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } + } else if (!group.value.sub) { + // Rule 18c, d + supShift = Math.max(supShift, minSupShift, + sup.depth + 0.25 * fontMetrics.metrics.xHeight); + + supsub = buildCommon.makeVList([ + {type: "elem", elem: supmid}, + ], "shift", -supShift, options); + + supsub.children[0].style.marginRight = scriptspace; + } else { + supShift = Math.max( + supShift, minSupShift, + sup.depth + 0.25 * fontMetrics.metrics.xHeight); + subShift = Math.max(subShift, fontMetrics.metrics.sub2); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness; + + // Rule 18e + if ((supShift - sup.depth) - (sub.height - subShift) < + 4 * ruleWidth) { + subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height; + var psi = 0.8 * fontMetrics.metrics.xHeight - + (supShift - sup.depth); + if (psi > 0) { + supShift += psi; + subShift -= psi; + } + } + + supsub = buildCommon.makeVList([ + {type: "elem", elem: submid, shift: subShift}, + {type: "elem", elem: supmid, shift: -supShift}, + ], "individualShift", null, options); + + // See comment above about subscripts not being shifted + if (base instanceof domTree.symbolNode) { + supsub.children[0].style.marginLeft = -base.italic + "em"; + } + + supsub.children[0].style.marginRight = scriptspace; + supsub.children[1].style.marginRight = scriptspace; + } + + // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align + return makeSpan([getTypeOfGroup(group.value.base)], + [base, makeSpan(["msupsub"], [supsub])]); +}; + +groupTypes.genfrac = function(group, options, prev) { + // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). + // Figure out what style this fraction should be in based on the + // function used + var fstyle = options.style; + if (group.value.size === "display") { + fstyle = Style.DISPLAY; + } else if (group.value.size === "text") { + fstyle = Style.TEXT; + } + + var nstyle = fstyle.fracNum(); + var dstyle = fstyle.fracDen(); + + var numer = buildGroup(group.value.numer, options.withStyle(nstyle)); + var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]); + + var denom = buildGroup(group.value.denom, options.withStyle(dstyle)); + var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]); + + var ruleWidth; + if (group.value.hasBarLine) { + ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + } else { + ruleWidth = 0; + } + + // Rule 15b + var numShift; + var clearance; + var denomShift; + if (fstyle.size === Style.DISPLAY.size) { + numShift = fontMetrics.metrics.num1; + if (ruleWidth > 0) { + clearance = 3 * ruleWidth; + } else { + clearance = 7 * fontMetrics.metrics.defaultRuleThickness; + } + denomShift = fontMetrics.metrics.denom1; + } else { + if (ruleWidth > 0) { + numShift = fontMetrics.metrics.num2; + clearance = ruleWidth; + } else { + numShift = fontMetrics.metrics.num3; + clearance = 3 * fontMetrics.metrics.defaultRuleThickness; + } + denomShift = fontMetrics.metrics.denom2; + } + + var frac; + if (ruleWidth === 0) { + // Rule 15c + var candiateClearance = + (numShift - numer.depth) - (denom.height - denomShift); + if (candiateClearance < clearance) { + numShift += 0.5 * (clearance - candiateClearance); + denomShift += 0.5 * (clearance - candiateClearance); + } + + frac = buildCommon.makeVList([ + {type: "elem", elem: denomreset, shift: denomShift}, + {type: "elem", elem: numerreset, shift: -numShift}, + ], "individualShift", null, options); + } else { + // Rule 15d + var axisHeight = fontMetrics.metrics.axisHeight; + + if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) < + clearance) { + numShift += + clearance - ((numShift - numer.depth) - + (axisHeight + 0.5 * ruleWidth)); + } + + if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) < + clearance) { + denomShift += + clearance - ((axisHeight - 0.5 * ruleWidth) - + (denom.height - denomShift)); + } + + var mid = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "frac-line"]); + // Manually set the height of the line because its height is + // created in CSS + mid.height = ruleWidth; + + var midShift = -(axisHeight - 0.5 * ruleWidth); + + frac = buildCommon.makeVList([ + {type: "elem", elem: denomreset, shift: denomShift}, + {type: "elem", elem: mid, shift: midShift}, + {type: "elem", elem: numerreset, shift: -numShift}, + ], "individualShift", null, options); + } + + // Since we manually change the style sometimes (with \dfrac or \tfrac), + // account for the possible size change here. + frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier; + frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier; + + // Rule 15e + var delimSize; + if (fstyle.size === Style.DISPLAY.size) { + delimSize = fontMetrics.metrics.delim1; + } else { + delimSize = fontMetrics.metrics.getDelim2(fstyle); + } + + var leftDelim; + var rightDelim; + if (group.value.leftDelim == null) { + leftDelim = makeNullDelimiter(options); + } else { + leftDelim = delimiter.customSizedDelim( + group.value.leftDelim, delimSize, true, + options.withStyle(fstyle), group.mode); + } + if (group.value.rightDelim == null) { + rightDelim = makeNullDelimiter(options); + } else { + rightDelim = delimiter.customSizedDelim( + group.value.rightDelim, delimSize, true, + options.withStyle(fstyle), group.mode); + } + + return makeSpan( + ["mord", options.style.reset(), fstyle.cls()], + [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim], + options.getColor()); +}; + +groupTypes.array = function(group, options, prev) { + var r; + var c; + var nr = group.value.body.length; + var nc = 0; + var body = new Array(nr); + + // Horizontal spacing + var pt = 1 / fontMetrics.metrics.ptPerEm; + var arraycolsep = 5 * pt; // \arraycolsep in article.cls + + // Vertical spacing + var baselineskip = 12 * pt; // see size10.clo + // Default \arraystretch from lttab.dtx + // TODO(gagern): may get redefined once we have user-defined macros + var arraystretch = utils.deflt(group.value.arraystretch, 1); + var arrayskip = arraystretch * baselineskip; + var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and + var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx + + var totalHeight = 0; + for (r = 0; r < group.value.body.length; ++r) { + var inrow = group.value.body[r]; + var height = arstrutHeight; // \@array adds an \@arstrut + var depth = arstrutDepth; // to each tow (via the template) + + if (nc < inrow.length) { + nc = inrow.length; + } + + var outrow = new Array(inrow.length); + for (c = 0; c < inrow.length; ++c) { + var elt = buildGroup(inrow[c], options); + if (depth < elt.depth) { + depth = elt.depth; + } + if (height < elt.height) { + height = elt.height; + } + outrow[c] = elt; + } + + var gap = 0; + if (group.value.rowGaps[r]) { + gap = group.value.rowGaps[r].value; + switch (gap.unit) { + case "em": + gap = gap.number; + break; + case "ex": + gap = gap.number * fontMetrics.metrics.emPerEx; + break; + default: + console.error("Can't handle unit " + gap.unit); + gap = 0; + } + if (gap > 0) { // \@argarraycr + gap += arstrutDepth; + if (depth < gap) { + depth = gap; // \@xargarraycr + } + gap = 0; + } + } + + outrow.height = height; + outrow.depth = depth; + totalHeight += height; + outrow.pos = totalHeight; + totalHeight += depth + gap; // \@yargarraycr + body[r] = outrow; + } + + var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight; + var colDescriptions = group.value.cols || []; + var cols = []; + var colSep; + var colDescrNum; + for (c = 0, colDescrNum = 0; + // Continue while either there are more columns or more column + // descriptions, so trailing separators don't get lost. + c < nc || colDescrNum < colDescriptions.length; + ++c, ++colDescrNum) { + + var colDescr = colDescriptions[colDescrNum] || {}; + + var firstSeparator = true; + while (colDescr.type === "separator") { + // If there is more than one separator in a row, add a space + // between them. + if (!firstSeparator) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = + fontMetrics.metrics.doubleRuleSep + "em"; + cols.push(colSep); + } + + if (colDescr.separator === "|") { + var separator = makeSpan( + ["vertical-separator"], + []); + separator.style.height = totalHeight + "em"; + separator.style.verticalAlign = + -(totalHeight - offset) + "em"; + + cols.push(separator); + } else { + throw new ParseError( + "Invalid separator type: " + colDescr.separator); + } + + colDescrNum++; + colDescr = colDescriptions[colDescrNum] || {}; + firstSeparator = false; + } + + if (c >= nc) { + continue; + } + + var sepwidth; + if (c > 0 || group.value.hskipBeforeAndAfter) { + sepwidth = utils.deflt(colDescr.pregap, arraycolsep); + if (sepwidth !== 0) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = sepwidth + "em"; + cols.push(colSep); + } + } + + var col = []; + for (r = 0; r < nr; ++r) { + var row = body[r]; + var elem = row[c]; + if (!elem) { + continue; + } + var shift = row.pos - offset; + elem.depth = row.depth; + elem.height = row.height; + col.push({type: "elem", elem: elem, shift: shift}); + } + + col = buildCommon.makeVList(col, "individualShift", null, options); + col = makeSpan( + ["col-align-" + (colDescr.align || "c")], + [col]); + cols.push(col); + + if (c < nc - 1 || group.value.hskipBeforeAndAfter) { + sepwidth = utils.deflt(colDescr.postgap, arraycolsep); + if (sepwidth !== 0) { + colSep = makeSpan(["arraycolsep"], []); + colSep.style.width = sepwidth + "em"; + cols.push(colSep); + } + } + } + body = makeSpan(["mtable"], cols); + return makeSpan(["mord"], [body], options.getColor()); +}; + +groupTypes.spacing = function(group, options, prev) { + if (group.value === "\\ " || group.value === "\\space" || + group.value === " " || group.value === "~") { + // Spaces are generated by adding an actual space. Each of these + // things has an entry in the symbols table, so these will be turned + // into appropriate outputs. + return makeSpan( + ["mord", "mspace"], + [buildCommon.mathsym(group.value, group.mode)] + ); + } else { + // Other kinds of spaces are of arbitrary width. We use CSS to + // generate these. + return makeSpan( + ["mord", "mspace", + buildCommon.spacingFunctions[group.value].className]); + } +}; + +groupTypes.llap = function(group, options, prev) { + var inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.reset())]); + var fix = makeSpan(["fix"], []); + return makeSpan( + ["llap", options.style.cls()], [inner, fix]); +}; + +groupTypes.rlap = function(group, options, prev) { + var inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.reset())]); + var fix = makeSpan(["fix"], []); + return makeSpan( + ["rlap", options.style.cls()], [inner, fix]); +}; + +groupTypes.op = function(group, options, prev) { + // Operators are handled in the TeXbook pg. 443-444, rule 13(a). + var supGroup; + var subGroup; + var hasLimits = false; + if (group.type === "supsub" ) { + // If we have limits, supsub will pass us its group to handle. Pull + // out the superscript and subscript and set the group to the op in + // its base. + supGroup = group.value.sup; + subGroup = group.value.sub; + group = group.value.base; + hasLimits = true; + } + + // Most operators have a large successor symbol, but these don't. + var noSuccessor = [ + "\\smallint", + ]; + + var large = false; + if (options.style.size === Style.DISPLAY.size && + group.value.symbol && + !utils.contains(noSuccessor, group.value.body)) { + + // Most symbol operators get larger in displaystyle (rule 13) + large = true; + } + + var base; + var baseShift = 0; + var slant = 0; + if (group.value.symbol) { + // If this is a symbol, create the symbol. + var style = large ? "Size2-Regular" : "Size1-Regular"; + base = buildCommon.makeSymbol( + group.value.body, style, "math", options.getColor(), + ["op-symbol", large ? "large-op" : "small-op", "mop"]); + + // Shift the symbol so its center lies on the axis (rule 13). It + // appears that our fonts have the centers of the symbols already + // almost on the axis, so these numbers are very small. Note we + // don't actually apply this here, but instead it is used either in + // the vlist creation or separately when there are no limits. + baseShift = (base.height - base.depth) / 2 - + fontMetrics.metrics.axisHeight * + options.style.sizeMultiplier; + + // The slant of the symbol is just its italic correction. + slant = base.italic; + } else { + // Otherwise, this is a text operator. Build the text from the + // operator's name. + // TODO(emily): Add a space in the middle of some of these + // operators, like \limsup + var output = []; + for (var i = 1; i < group.value.body.length; i++) { + output.push(buildCommon.mathsym(group.value.body[i], group.mode)); + } + base = makeSpan(["mop"], output, options.getColor()); + } + + if (hasLimits) { + // IE 8 clips \int if it is in a display: inline-block. We wrap it + // in a new span so it is an inline, and works. + base = makeSpan([], [base]); + + var supmid; + var supKern; + var submid; + var subKern; + // We manually have to handle the superscripts and subscripts. This, + // aside from the kern calculations, is copied from supsub. + if (supGroup) { + var sup = buildGroup( + supGroup, options.withStyle(options.style.sup())); + supmid = makeSpan( + [options.style.reset(), options.style.sup().cls()], [sup]); + + supKern = Math.max( + fontMetrics.metrics.bigOpSpacing1, + fontMetrics.metrics.bigOpSpacing3 - sup.depth); + } + + if (subGroup) { + var sub = buildGroup( + subGroup, options.withStyle(options.style.sub())); + submid = makeSpan( + [options.style.reset(), options.style.sub().cls()], + [sub]); + + subKern = Math.max( + fontMetrics.metrics.bigOpSpacing2, + fontMetrics.metrics.bigOpSpacing4 - sub.height); + } + + // Build the final group as a vlist of the possible subscript, base, + // and possible superscript. + var finalGroup; + var top; + var bottom; + if (!supGroup) { + top = base.height - baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "elem", elem: submid}, + {type: "kern", size: subKern}, + {type: "elem", elem: base}, + ], "top", top, options); + + // Here, we shift the limits by the slant of the symbol. Note + // that we are supposed to shift the limits by 1/2 of the slant, + // but since we are centering the limits adding a full slant of + // margin will shift by 1/2 that. + finalGroup.children[0].style.marginLeft = -slant + "em"; + } else if (!subGroup) { + bottom = base.depth + baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "elem", elem: base}, + {type: "kern", size: supKern}, + {type: "elem", elem: supmid}, + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + ], "bottom", bottom, options); + + // See comment above about slants + finalGroup.children[1].style.marginLeft = slant + "em"; + } else if (!supGroup && !subGroup) { + // This case probably shouldn't occur (this would mean the + // supsub was sending us a group with no superscript or + // subscript) but be safe. + return base; + } else { + bottom = fontMetrics.metrics.bigOpSpacing5 + + submid.height + submid.depth + + subKern + + base.depth + baseShift; + + finalGroup = buildCommon.makeVList([ + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "elem", elem: submid}, + {type: "kern", size: subKern}, + {type: "elem", elem: base}, + {type: "kern", size: supKern}, + {type: "elem", elem: supmid}, + {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + ], "bottom", bottom, options); + + // See comment above about slants + finalGroup.children[0].style.marginLeft = -slant + "em"; + finalGroup.children[2].style.marginLeft = slant + "em"; + } + + return makeSpan(["mop", "op-limits"], [finalGroup]); + } else { + if (group.value.symbol) { + base.style.top = baseShift + "em"; + } + + return base; + } +}; + +groupTypes.katex = function(group, options, prev) { + // The KaTeX logo. The offsets for the K and a were chosen to look + // good, but the offsets for the T, E, and X were taken from the + // definition of \TeX in TeX (see TeXbook pg. 356) + var k = makeSpan( + ["k"], [buildCommon.mathsym("K", group.mode)]); + var a = makeSpan( + ["a"], [buildCommon.mathsym("A", group.mode)]); + + a.height = (a.height + 0.2) * 0.75; + a.depth = (a.height - 0.2) * 0.75; + + var t = makeSpan( + ["t"], [buildCommon.mathsym("T", group.mode)]); + var e = makeSpan( + ["e"], [buildCommon.mathsym("E", group.mode)]); + + e.height = (e.height - 0.2155); + e.depth = (e.depth + 0.2155); + + var x = makeSpan( + ["x"], [buildCommon.mathsym("X", group.mode)]); + + return makeSpan( + ["katex-logo", "mord"], [k, a, t, e, x], options.getColor()); +}; + +groupTypes.overline = function(group, options, prev) { + // Overlines are handled in the TeXbook pg 443, Rule 9. + + // Build the inner group in the cramped style. + var innerGroup = buildGroup(group.value.body, + options.withStyle(options.style.cramp())); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + // Create the line above the body + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "overline-line"]); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + // Generate the vlist, with the appropriate kerns + var vlist = buildCommon.makeVList([ + {type: "elem", elem: innerGroup}, + {type: "kern", size: 3 * ruleWidth}, + {type: "elem", elem: line}, + {type: "kern", size: ruleWidth}, + ], "firstBaseline", null, options); + + return makeSpan(["overline", "mord"], [vlist], options.getColor()); +}; + +groupTypes.underline = function(group, options, prev) { + // Underlines are handled in the TeXbook pg 443, Rule 10. + + // Build the inner group. + var innerGroup = buildGroup(group.value.body, options); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + // Create the line above the body + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "underline-line"]); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + // Generate the vlist, with the appropriate kerns + var vlist = buildCommon.makeVList([ + {type: "kern", size: ruleWidth}, + {type: "elem", elem: line}, + {type: "kern", size: 3 * ruleWidth}, + {type: "elem", elem: innerGroup}, + ], "top", innerGroup.height, options); + + return makeSpan(["underline", "mord"], [vlist], options.getColor()); +}; + +groupTypes.sqrt = function(group, options, prev) { + // Square roots are handled in the TeXbook pg. 443, Rule 11. + + // First, we do the same steps as in overline to build the inner group + // and line + var inner = buildGroup(group.value.body, + options.withStyle(options.style.cramp())); + + var ruleWidth = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + + var line = makeSpan( + [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [], + options.getColor()); + line.height = ruleWidth; + line.maxFontSize = 1.0; + + var phi = ruleWidth; + if (options.style.id < Style.TEXT.id) { + phi = fontMetrics.metrics.xHeight; + } + + // Calculate the clearance between the body and line + var lineClearance = ruleWidth + phi / 4; + + var innerHeight = + (inner.height + inner.depth) * options.style.sizeMultiplier; + var minDelimiterHeight = innerHeight + lineClearance + ruleWidth; + + // Create a \surd delimiter of the required minimum size + var delim = makeSpan(["sqrt-sign"], [ + delimiter.customSizedDelim("\\surd", minDelimiterHeight, + false, options, group.mode)], + options.getColor()); + + var delimDepth = (delim.height + delim.depth) - ruleWidth; + + // Adjust the clearance based on the delimiter size + if (delimDepth > inner.height + inner.depth + lineClearance) { + lineClearance = + (lineClearance + delimDepth - inner.height - inner.depth) / 2; + } + + // Shift the delimiter so that its top lines up with the top of the line + var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height; + delim.style.top = delimShift + "em"; + delim.height -= delimShift; + delim.depth += delimShift; + + // We add a special case here, because even when `inner` is empty, we + // still get a line. So, we use a simple heuristic to decide if we + // should omit the body entirely. (note this doesn't work for something + // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for + // it not to work. + var body; + if (inner.height === 0 && inner.depth === 0) { + body = makeSpan(); + } else { + body = buildCommon.makeVList([ + {type: "elem", elem: inner}, + {type: "kern", size: lineClearance}, + {type: "elem", elem: line}, + {type: "kern", size: ruleWidth}, + ], "firstBaseline", null, options); + } + + if (!group.value.index) { + return makeSpan(["sqrt", "mord"], [delim, body]); + } else { + // Handle the optional root index + + // The index is always in scriptscript style + var root = buildGroup( + group.value.index, + options.withStyle(Style.SCRIPTSCRIPT)); + var rootWrap = makeSpan( + [options.style.reset(), Style.SCRIPTSCRIPT.cls()], + [root]); + + // Figure out the height and depth of the inner part + var innerRootHeight = Math.max(delim.height, body.height); + var innerRootDepth = Math.max(delim.depth, body.depth); + + // The amount the index is shifted by. This is taken from the TeX + // source, in the definition of `\r@@t`. + var toShift = 0.6 * (innerRootHeight - innerRootDepth); + + // Build a VList with the superscript shifted up correctly + var rootVList = buildCommon.makeVList( + [{type: "elem", elem: rootWrap}], + "shift", -toShift, options); + // Add a class surrounding it so we can add on the appropriate + // kerning + var rootVListWrap = makeSpan(["root"], [rootVList]); + + return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]); + } +}; + +groupTypes.sizing = function(group, options, prev) { + // Handle sizing operators like \Huge. Real TeX doesn't actually allow + // these functions inside of math expressions, so we do some special + // handling. + var inner = buildExpression(group.value.value, + options.withSize(group.value.size), prev); + + var span = makeSpan(["mord"], + [makeSpan(["sizing", "reset-" + options.size, group.value.size, + options.style.cls()], + inner)]); + + // Calculate the correct maxFontSize manually + var fontSize = buildCommon.sizingMultiplier[group.value.size]; + span.maxFontSize = fontSize * options.style.sizeMultiplier; + + return span; +}; + +groupTypes.styling = function(group, options, prev) { + // Style changes are handled in the TeXbook on pg. 442, Rule 3. + + // Figure out what style we're changing to. + var style = { + "display": Style.DISPLAY, + "text": Style.TEXT, + "script": Style.SCRIPT, + "scriptscript": Style.SCRIPTSCRIPT, + }; + + var newStyle = style[group.value.style]; + + // Build the inner expression in the new style. + var inner = buildExpression( + group.value.value, options.withStyle(newStyle), prev); + + return makeSpan([options.style.reset(), newStyle.cls()], inner); +}; + +groupTypes.font = function(group, options, prev) { + var font = group.value.font; + return buildGroup(group.value.body, options.withFont(font), prev); +}; + +groupTypes.delimsizing = function(group, options, prev) { + var delim = group.value.value; + + if (delim === ".") { + // Empty delimiters still count as elements, even though they don't + // show anything. + return makeSpan([groupToType[group.value.delimType]]); + } + + // Use delimiter.sizedDelim to generate the delimiter. + return makeSpan( + [groupToType[group.value.delimType]], + [delimiter.sizedDelim( + delim, group.value.size, options, group.mode)]); +}; + +groupTypes.leftright = function(group, options, prev) { + // Build the inner expression + var inner = buildExpression(group.value.body, options.reset()); + + var innerHeight = 0; + var innerDepth = 0; + + // Calculate its height and depth + for (var i = 0; i < inner.length; i++) { + innerHeight = Math.max(inner[i].height, innerHeight); + innerDepth = Math.max(inner[i].depth, innerDepth); + } + + // The size of delimiters is the same, regardless of what style we are + // in. Thus, to correctly calculate the size of delimiter we need around + // a group, we scale down the inner size based on the size. + innerHeight *= options.style.sizeMultiplier; + innerDepth *= options.style.sizeMultiplier; + + var leftDelim; + if (group.value.left === ".") { + // Empty delimiters in \left and \right make null delimiter spaces. + leftDelim = makeNullDelimiter(options); + } else { + // Otherwise, use leftRightDelim to generate the correct sized + // delimiter. + leftDelim = delimiter.leftRightDelim( + group.value.left, innerHeight, innerDepth, options, + group.mode); + } + // Add it to the beginning of the expression + inner.unshift(leftDelim); + + var rightDelim; + // Same for the right delimiter + if (group.value.right === ".") { + rightDelim = makeNullDelimiter(options); + } else { + rightDelim = delimiter.leftRightDelim( + group.value.right, innerHeight, innerDepth, options, + group.mode); + } + // Add it to the end of the expression. + inner.push(rightDelim); + + return makeSpan( + ["minner", options.style.cls()], inner, options.getColor()); +}; + +groupTypes.rule = function(group, options, prev) { + // Make an empty span for the rule + var rule = makeSpan(["mord", "rule"], [], options.getColor()); + + // Calculate the shift, width, and height of the rule, and account for units + var shift = 0; + if (group.value.shift) { + shift = group.value.shift.number; + if (group.value.shift.unit === "ex") { + shift *= fontMetrics.metrics.xHeight; + } + } + + var width = group.value.width.number; + if (group.value.width.unit === "ex") { + width *= fontMetrics.metrics.xHeight; + } + + var height = group.value.height.number; + if (group.value.height.unit === "ex") { + height *= fontMetrics.metrics.xHeight; + } + + // The sizes of rules are absolute, so make it larger if we are in a + // smaller style. + shift /= options.style.sizeMultiplier; + width /= options.style.sizeMultiplier; + height /= options.style.sizeMultiplier; + + // Style the rule to the right size + rule.style.borderRightWidth = width + "em"; + rule.style.borderTopWidth = height + "em"; + rule.style.bottom = shift + "em"; + + // Record the height and width + rule.width = width; + rule.height = height + shift; + rule.depth = -shift; + + return rule; +}; + +groupTypes.kern = function(group, options, prev) { + // Make an empty span for the rule + var rule = makeSpan(["mord", "rule"], [], options.getColor()); + + var dimension = 0; + if (group.value.dimension) { + dimension = group.value.dimension.number; + if (group.value.dimension.unit === "ex") { + dimension *= fontMetrics.metrics.xHeight; + } + } + + dimension /= options.style.sizeMultiplier; + + rule.style.marginLeft = dimension + "em"; + + return rule; +}; + +groupTypes.accent = function(group, options, prev) { + // Accents are handled in the TeXbook pg. 443, rule 12. + var base = group.value.base; + + var supsubGroup; + if (group.type === "supsub") { + // If our base is a character box, and we have superscripts and + // subscripts, the supsub will defer to us. In particular, we want + // to attach the superscripts and subscripts to the inner body (so + // that the position of the superscripts and subscripts won't be + // affected by the height of the accent). We accomplish this by + // sticking the base of the accent into the base of the supsub, and + // rendering that, while keeping track of where the accent is. + + // The supsub group is the group that was passed in + var supsub = group; + // The real accent group is the base of the supsub group + group = supsub.value.base; + // The character box is the base of the accent group + base = group.value.base; + // Stick the character box into the base of the supsub group + supsub.value.base = base; + + // Rerender the supsub group with its new base, and store that + // result. + supsubGroup = buildGroup( + supsub, options.reset(), prev); + } + + // Build the base group + var body = buildGroup( + base, options.withStyle(options.style.cramp())); + + // Calculate the skew of the accent. This is based on the line "If the + // nucleus is not a single character, let s = 0; otherwise set s to the + // kern amount for the nucleus followed by the \skewchar of its font." + // Note that our skew metrics are just the kern between each character + // and the skewchar. + var skew; + if (isCharacterBox(base)) { + // If the base is a character box, then we want the skew of the + // innermost character. To do that, we find the innermost character: + var baseChar = getBaseElem(base); + // Then, we render its group to get the symbol inside it + var baseGroup = buildGroup( + baseChar, options.withStyle(options.style.cramp())); + // Finally, we pull the skew off of the symbol. + skew = baseGroup.skew; + // Note that we now throw away baseGroup, because the layers we + // removed with getBaseElem might contain things like \color which + // we can't get rid of. + // TODO(emily): Find a better way to get the skew + } else { + skew = 0; + } + + // calculate the amount of space between the body and the accent + var clearance = Math.min(body.height, fontMetrics.metrics.xHeight); + + // Build the accent + var accent = buildCommon.makeSymbol( + group.value.accent, "Main-Regular", "math", options.getColor()); + // Remove the italic correction of the accent, because it only serves to + // shift the accent over to a place we don't want. + accent.italic = 0; + + // The \vec character that the fonts use is a combining character, and + // thus shows up much too far to the left. To account for this, we add a + // specific class which shifts the accent over to where we want it. + // TODO(emily): Fix this in a better way, like by changing the font + var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null; + + var accentBody = makeSpan(["accent-body", vecClass], [ + makeSpan([], [accent])]); + + accentBody = buildCommon.makeVList([ + {type: "elem", elem: body}, + {type: "kern", size: -clearance}, + {type: "elem", elem: accentBody}, + ], "firstBaseline", null, options); + + // Shift the accent over by the skew. Note we shift by twice the skew + // because we are centering the accent, so by adding 2*skew to the left, + // we shift it to the right by 1*skew. + accentBody.children[1].style.marginLeft = 2 * skew + "em"; + + var accentWrap = makeSpan(["mord", "accent"], [accentBody]); + + if (supsubGroup) { + // Here, we replace the "base" child of the supsub with our newly + // generated accent. + supsubGroup.children[0] = accentWrap; + + // Since we don't rerun the height calculation after replacing the + // accent, we manually recalculate height. + supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height); + + // Accents should always be ords, even when their innards are not. + supsubGroup.classes[0] = "mord"; + + return supsubGroup; + } else { + return accentWrap; + } +}; + +groupTypes.phantom = function(group, options, prev) { + var elements = buildExpression( + group.value.value, + options.withPhantom(), + prev + ); + + // \phantom isn't supposed to affect the elements it contains. + // See "color" for more details. + return new buildCommon.makeFragment(elements); +}; + +/** + * buildGroup is the function that takes a group and calls the correct groupType + * function for it. It also handles the interaction of size and style changes + * between parents and children. + */ +var buildGroup = function(group, options, prev) { + if (!group) { + return makeSpan(); + } + + if (groupTypes[group.type]) { + // Call the groupTypes function + var groupNode = groupTypes[group.type](group, options, prev); + var multiplier; + + // If the style changed between the parent and the current group, + // account for the size difference + if (options.style !== options.parentStyle) { + multiplier = options.style.sizeMultiplier / + options.parentStyle.sizeMultiplier; + + groupNode.height *= multiplier; + groupNode.depth *= multiplier; + } + + // If the size changed between the parent and the current group, account + // for that size difference. + if (options.size !== options.parentSize) { + multiplier = buildCommon.sizingMultiplier[options.size] / + buildCommon.sizingMultiplier[options.parentSize]; + + groupNode.height *= multiplier; + groupNode.depth *= multiplier; + } + + return groupNode; + } else { + throw new ParseError( + "Got group of unknown type: '" + group.type + "'"); + } +}; + +/** + * Take an entire parse tree, and build it into an appropriate set of HTML + * nodes. + */ +var buildHTML = function(tree, options) { + // buildExpression is destructive, so we need to make a clone + // of the incoming tree so that it isn't accidentally changed + tree = JSON.parse(JSON.stringify(tree)); + + // Build the expression contained in the tree + var expression = buildExpression(tree, options); + var body = makeSpan(["base", options.style.cls()], expression); + + // Add struts, which ensure that the top of the HTML element falls at the + // height of the expression, and the bottom of the HTML element falls at the + // depth of the expression. + var topStrut = makeSpan(["strut"]); + var bottomStrut = makeSpan(["strut", "bottom"]); + + topStrut.style.height = body.height + "em"; + bottomStrut.style.height = (body.height + body.depth) + "em"; + // We'd like to use `vertical-align: top` but in IE 9 this lowers the + // baseline of the box to the bottom of this strut (instead staying in the + // normal place) so we use an absolute value for vertical-align instead + bottomStrut.style.verticalAlign = -body.depth + "em"; + + // Wrap the struts and body together + var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]); + + htmlNode.setAttribute("aria-hidden", "true"); + + return htmlNode; +}; + +module.exports = buildHTML; + +},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./delimiter":14,"./domTree":15,"./fontMetrics":17,"./utils":25}],12:[function(require,module,exports){ +/** + * This file converts a parse tree into a cooresponding MathML tree. The main + * entry point is the `buildMathML` function, which takes a parse tree from the + * parser. + */ + +var buildCommon = require("./buildCommon"); +var fontMetrics = require("./fontMetrics"); +var mathMLTree = require("./mathMLTree"); +var ParseError = require("./ParseError"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; +var fontMap = buildCommon.fontMap; + +/** + * Takes a symbol and converts it into a MathML text node after performing + * optional replacement from symbols.js. + */ +var makeText = function(text, mode) { + if (symbols[mode][text] && symbols[mode][text].replace) { + text = symbols[mode][text].replace; + } + + return new mathMLTree.TextNode(text); +}; + +/** + * Returns the math variant as a string or null if none is required. + */ +var getVariant = function(group, options) { + var font = options.font; + if (!font) { + return null; + } + + var mode = group.mode; + if (font === "mathit") { + return "italic"; + } + + var value = group.value; + if (utils.contains(["\\imath", "\\jmath"], value)) { + return null; + } + + if (symbols[mode][value] && symbols[mode][value].replace) { + value = symbols[mode][value].replace; + } + + var fontName = fontMap[font].fontName; + if (fontMetrics.getCharacterMetrics(value, fontName)) { + return fontMap[options.font].variant; + } + + return null; +}; + +/** + * Functions for handling the different types of groups found in the parse + * tree. Each function should take a parse group and return a MathML node. + */ +var groupTypes = {}; + +groupTypes.mathord = function(group, options) { + var node = new mathMLTree.MathNode( + "mi", + [makeText(group.value, group.mode)]); + + var variant = getVariant(group, options); + if (variant) { + node.setAttribute("mathvariant", variant); + } + return node; +}; + +groupTypes.textord = function(group, options) { + var text = makeText(group.value, group.mode); + + var variant = getVariant(group, options) || "normal"; + + var node; + if (/[0-9]/.test(group.value)) { + // TODO(kevinb) merge adjacent <mn> nodes + // do it as a post processing step + node = new mathMLTree.MathNode("mn", [text]); + if (options.font) { + node.setAttribute("mathvariant", variant); + } + } else { + node = new mathMLTree.MathNode("mi", [text]); + node.setAttribute("mathvariant", variant); + } + + return node; +}; + +groupTypes.bin = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.rel = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.open = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.close = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.inner = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + return node; +}; + +groupTypes.punct = function(group) { + var node = new mathMLTree.MathNode( + "mo", [makeText(group.value, group.mode)]); + + node.setAttribute("separator", "true"); + + return node; +}; + +groupTypes.ordgroup = function(group, options) { + var inner = buildExpression(group.value, options); + + var node = new mathMLTree.MathNode("mrow", inner); + + return node; +}; + +groupTypes.text = function(group, options) { + var inner = buildExpression(group.value.body, options); + + var node = new mathMLTree.MathNode("mtext", inner); + + return node; +}; + +groupTypes.color = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + node.setAttribute("mathcolor", group.value.color); + + return node; +}; + +groupTypes.supsub = function(group, options) { + var children = [buildGroup(group.value.base, options)]; + + if (group.value.sub) { + children.push(buildGroup(group.value.sub, options)); + } + + if (group.value.sup) { + children.push(buildGroup(group.value.sup, options)); + } + + var nodeType; + if (!group.value.sub) { + nodeType = "msup"; + } else if (!group.value.sup) { + nodeType = "msub"; + } else { + nodeType = "msubsup"; + } + + var node = new mathMLTree.MathNode(nodeType, children); + + return node; +}; + +groupTypes.genfrac = function(group, options) { + var node = new mathMLTree.MathNode( + "mfrac", + [buildGroup(group.value.numer, options), + buildGroup(group.value.denom, options)]); + + if (!group.value.hasBarLine) { + node.setAttribute("linethickness", "0px"); + } + + if (group.value.leftDelim != null || group.value.rightDelim != null) { + var withDelims = []; + + if (group.value.leftDelim != null) { + var leftOp = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode(group.value.leftDelim)]); + + leftOp.setAttribute("fence", "true"); + + withDelims.push(leftOp); + } + + withDelims.push(node); + + if (group.value.rightDelim != null) { + var rightOp = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode(group.value.rightDelim)]); + + rightOp.setAttribute("fence", "true"); + + withDelims.push(rightOp); + } + + var outerNode = new mathMLTree.MathNode("mrow", withDelims); + + return outerNode; + } + + return node; +}; + +groupTypes.array = function(group, options) { + return new mathMLTree.MathNode( + "mtable", group.value.body.map(function(row) { + return new mathMLTree.MathNode( + "mtr", row.map(function(cell) { + return new mathMLTree.MathNode( + "mtd", [buildGroup(cell, options)]); + })); + })); +}; + +groupTypes.sqrt = function(group, options) { + var node; + if (group.value.index) { + node = new mathMLTree.MathNode( + "mroot", [ + buildGroup(group.value.body, options), + buildGroup(group.value.index, options), + ]); + } else { + node = new mathMLTree.MathNode( + "msqrt", [buildGroup(group.value.body, options)]); + } + + return node; +}; + +groupTypes.leftright = function(group, options) { + var inner = buildExpression(group.value.body, options); + + if (group.value.left !== ".") { + var leftNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.left, group.mode)]); + + leftNode.setAttribute("fence", "true"); + + inner.unshift(leftNode); + } + + if (group.value.right !== ".") { + var rightNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.right, group.mode)]); + + rightNode.setAttribute("fence", "true"); + + inner.push(rightNode); + } + + var outerNode = new mathMLTree.MathNode("mrow", inner); + + return outerNode; +}; + +groupTypes.accent = function(group, options) { + var accentNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.accent, group.mode)]); + + var node = new mathMLTree.MathNode( + "mover", + [buildGroup(group.value.base, options), + accentNode]); + + node.setAttribute("accent", "true"); + + return node; +}; + +groupTypes.spacing = function(group) { + var node; + + if (group.value === "\\ " || group.value === "\\space" || + group.value === " " || group.value === "~") { + node = new mathMLTree.MathNode( + "mtext", [new mathMLTree.TextNode("\u00a0")]); + } else { + node = new mathMLTree.MathNode("mspace"); + + node.setAttribute( + "width", buildCommon.spacingFunctions[group.value].size); + } + + return node; +}; + +groupTypes.op = function(group) { + var node; + + // TODO(emily): handle big operators using the `largeop` attribute + + if (group.value.symbol) { + // This is a symbol. Just add the symbol. + node = new mathMLTree.MathNode( + "mo", [makeText(group.value.body, group.mode)]); + } else { + // This is a text operator. Add all of the characters from the + // operator's name. + // TODO(emily): Add a space in the middle of some of these + // operators, like \limsup. + node = new mathMLTree.MathNode( + "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]); + } + + return node; +}; + +groupTypes.katex = function(group) { + var node = new mathMLTree.MathNode( + "mtext", [new mathMLTree.TextNode("KaTeX")]); + + return node; +}; + +groupTypes.font = function(group, options) { + var font = group.value.font; + return buildGroup(group.value.body, options.withFont(font)); +}; + +groupTypes.delimsizing = function(group) { + var children = []; + + if (group.value.value !== ".") { + children.push(makeText(group.value.value, group.mode)); + } + + var node = new mathMLTree.MathNode("mo", children); + + if (group.value.delimType === "open" || + group.value.delimType === "close") { + // Only some of the delimsizing functions act as fences, and they + // return "open" or "close" delimTypes. + node.setAttribute("fence", "true"); + } else { + // Explicitly disable fencing if it's not a fence, to override the + // defaults. + node.setAttribute("fence", "false"); + } + + return node; +}; + +groupTypes.styling = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + var styleAttributes = { + "display": ["0", "true"], + "text": ["0", "false"], + "script": ["1", "false"], + "scriptscript": ["2", "false"], + }; + + var attr = styleAttributes[group.value.style]; + + node.setAttribute("scriptlevel", attr[0]); + node.setAttribute("displaystyle", attr[1]); + + return node; +}; + +groupTypes.sizing = function(group, options) { + var inner = buildExpression(group.value.value, options); + + var node = new mathMLTree.MathNode("mstyle", inner); + + // TODO(emily): This doesn't produce the correct size for nested size + // changes, because we don't keep state of what style we're currently + // in, so we can't reset the size to normal before changing it. Now + // that we're passing an options parameter we should be able to fix + // this. + node.setAttribute( + "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); + + return node; +}; + +groupTypes.overline = function(group, options) { + var operator = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode("\u203e")]); + operator.setAttribute("stretchy", "true"); + + var node = new mathMLTree.MathNode( + "mover", + [buildGroup(group.value.body, options), + operator]); + node.setAttribute("accent", "true"); + + return node; +}; + +groupTypes.underline = function(group, options) { + var operator = new mathMLTree.MathNode( + "mo", [new mathMLTree.TextNode("\u203e")]); + operator.setAttribute("stretchy", "true"); + + var node = new mathMLTree.MathNode( + "munder", + [buildGroup(group.value.body, options), + operator]); + node.setAttribute("accentunder", "true"); + + return node; +}; + +groupTypes.rule = function(group) { + // TODO(emily): Figure out if there's an actual way to draw black boxes + // in MathML. + var node = new mathMLTree.MathNode("mrow"); + + return node; +}; + +groupTypes.kern = function(group) { + // TODO(kevin): Figure out if there's a way to add space in MathML + var node = new mathMLTree.MathNode("mrow"); + + return node; +}; + +groupTypes.llap = function(group, options) { + var node = new mathMLTree.MathNode( + "mpadded", [buildGroup(group.value.body, options)]); + + node.setAttribute("lspace", "-1width"); + node.setAttribute("width", "0px"); + + return node; +}; + +groupTypes.rlap = function(group, options) { + var node = new mathMLTree.MathNode( + "mpadded", [buildGroup(group.value.body, options)]); + + node.setAttribute("width", "0px"); + + return node; +}; + +groupTypes.phantom = function(group, options, prev) { + var inner = buildExpression(group.value.value, options); + return new mathMLTree.MathNode("mphantom", inner); +}; + +/** + * Takes a list of nodes, builds them, and returns a list of the generated + * MathML nodes. A little simpler than the HTML version because we don't do any + * previous-node handling. + */ +var buildExpression = function(expression, options) { + var groups = []; + for (var i = 0; i < expression.length; i++) { + var group = expression[i]; + groups.push(buildGroup(group, options)); + } + return groups; +}; + +/** + * Takes a group from the parser and calls the appropriate groupTypes function + * on it to produce a MathML node. + */ +var buildGroup = function(group, options) { + if (!group) { + return new mathMLTree.MathNode("mrow"); + } + + if (groupTypes[group.type]) { + // Call the groupTypes function + return groupTypes[group.type](group, options); + } else { + throw new ParseError( + "Got group of unknown type: '" + group.type + "'"); + } +}; + +/** + * Takes a full parse tree and settings and builds a MathML representation of + * it. In particular, we put the elements from building the parse tree into a + * <semantics> tag so we can also include that TeX source as an annotation. + * + * Note that we actually return a domTree element with a `<math>` inside it so + * we can do appropriate styling. + */ +var buildMathML = function(tree, texExpression, options) { + var expression = buildExpression(tree, options); + + // Wrap up the expression in an mrow so it is presented in the semantics + // tag correctly. + var wrapper = new mathMLTree.MathNode("mrow", expression); + + // Build a TeX annotation of the source + var annotation = new mathMLTree.MathNode( + "annotation", [new mathMLTree.TextNode(texExpression)]); + + annotation.setAttribute("encoding", "application/x-tex"); + + var semantics = new mathMLTree.MathNode( + "semantics", [wrapper, annotation]); + + var math = new mathMLTree.MathNode("math", [semantics]); + + // You can't style <math> nodes, so we wrap the node in a span. + return makeSpan(["katex-mathml"], [math]); +}; + +module.exports = buildMathML; + +},{"./ParseError":6,"./buildCommon":10,"./fontMetrics":17,"./mathMLTree":20,"./symbols":23,"./utils":25}],13:[function(require,module,exports){ +var buildHTML = require("./buildHTML"); +var buildMathML = require("./buildMathML"); +var buildCommon = require("./buildCommon"); +var Options = require("./Options"); +var Settings = require("./Settings"); +var Style = require("./Style"); + +var makeSpan = buildCommon.makeSpan; + +var buildTree = function(tree, expression, settings) { + settings = settings || new Settings({}); + + var startStyle = Style.TEXT; + if (settings.displayMode) { + startStyle = Style.DISPLAY; + } + + // Setup the default options + var options = new Options({ + style: startStyle, + size: "size5", + }); + + // `buildHTML` sometimes messes with the parse tree (like turning bins -> + // ords), so we build the MathML version first. + var mathMLNode = buildMathML(tree, expression, options); + var htmlNode = buildHTML(tree, options); + + var katexNode = makeSpan(["katex"], [ + mathMLNode, htmlNode, + ]); + + if (settings.displayMode) { + return makeSpan(["katex-display"], [katexNode]); + } else { + return katexNode; + } +}; + +module.exports = buildTree; + +},{"./Options":5,"./Settings":8,"./Style":9,"./buildCommon":10,"./buildHTML":11,"./buildMathML":12}],14:[function(require,module,exports){ +/** + * This file deals with creating delimiters of various sizes. The TeXbook + * discusses these routines on page 441-442, in the "Another subroutine sets box + * x to a specified variable delimiter" paragraph. + * + * There are three main routines here. `makeSmallDelim` makes a delimiter in the + * normal font, but in either text, script, or scriptscript style. + * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1, + * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of + * smaller pieces that are stacked on top of one another. + * + * The functions take a parameter `center`, which determines if the delimiter + * should be centered around the axis. + * + * Then, there are three exposed functions. `sizedDelim` makes a delimiter in + * one of the given sizes. This is used for things like `\bigl`. + * `customSizedDelim` makes a delimiter with a given total height+depth. It is + * called in places like `\sqrt`. `leftRightDelim` makes an appropriate + * delimiter which surrounds an expression of a given height an depth. It is + * used in `\left` and `\right`. + */ + +var ParseError = require("./ParseError"); +var Style = require("./Style"); + +var buildCommon = require("./buildCommon"); +var fontMetrics = require("./fontMetrics"); +var symbols = require("./symbols"); +var utils = require("./utils"); + +var makeSpan = buildCommon.makeSpan; + +/** + * Get the metrics for a given symbol and font, after transformation (i.e. + * after following replacement from symbols.js) + */ +var getMetrics = function(symbol, font) { + if (symbols.math[symbol] && symbols.math[symbol].replace) { + return fontMetrics.getCharacterMetrics( + symbols.math[symbol].replace, font); + } else { + return fontMetrics.getCharacterMetrics( + symbol, font); + } +}; + +/** + * Builds a symbol in the given font size (note size is an integer) + */ +var mathrmSize = function(value, size, mode) { + return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode); +}; + +/** + * Puts a delimiter span in a given style, and adds appropriate height, depth, + * and maxFontSizes. + */ +var styleWrap = function(delim, toStyle, options) { + var span = makeSpan( + ["style-wrap", options.style.reset(), toStyle.cls()], [delim]); + + var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier; + + span.height *= multiplier; + span.depth *= multiplier; + span.maxFontSize = toStyle.sizeMultiplier; + + return span; +}; + +/** + * Makes a small delimiter. This is a delimiter that comes in the Main-Regular + * font, but is restyled to either be in textstyle, scriptstyle, or + * scriptscriptstyle. + */ +var makeSmallDelim = function(delim, style, center, options, mode) { + var text = buildCommon.makeSymbol(delim, "Main-Regular", mode); + + var span = styleWrap(text, style, options); + + if (center) { + var shift = + (1 - options.style.sizeMultiplier / style.sizeMultiplier) * + fontMetrics.metrics.axisHeight; + + span.style.top = shift + "em"; + span.height -= shift; + span.depth += shift; + } + + return span; +}; + +/** + * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2, + * Size3, or Size4 fonts. It is always rendered in textstyle. + */ +var makeLargeDelim = function(delim, size, center, options, mode) { + var inner = mathrmSize(delim, size, mode); + + var span = styleWrap( + makeSpan(["delimsizing", "size" + size], + [inner], options.getColor()), + Style.TEXT, options); + + if (center) { + var shift = (1 - options.style.sizeMultiplier) * + fontMetrics.metrics.axisHeight; + + span.style.top = shift + "em"; + span.height -= shift; + span.depth += shift; + } + + return span; +}; + +/** + * Make an inner span with the given offset and in the given font. This is used + * in `makeStackedDelim` to make the stacking pieces for the delimiter. + */ +var makeInner = function(symbol, font, mode) { + var sizeClass; + // Apply the correct CSS class to choose the right font. + if (font === "Size1-Regular") { + sizeClass = "delim-size1"; + } else if (font === "Size4-Regular") { + sizeClass = "delim-size4"; + } + + var inner = makeSpan( + ["delimsizinginner", sizeClass], + [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); + + // Since this will be passed into `makeVList` in the end, wrap the element + // in the appropriate tag that VList uses. + return {type: "elem", elem: inner}; +}; + +/** + * Make a stacked delimiter out of a given delimiter, with the total height at + * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook. + */ +var makeStackedDelim = function(delim, heightTotal, center, options, mode) { + // There are four parts, the top, an optional middle, a repeated part, and a + // bottom. + var top; + var middle; + var repeat; + var bottom; + top = repeat = bottom = delim; + middle = null; + // Also keep track of what font the delimiters are in + var font = "Size1-Regular"; + + // We set the parts and font based on the symbol. Note that we use + // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the + // repeats of the arrows + if (delim === "\\uparrow") { + repeat = bottom = "\u23d0"; + } else if (delim === "\\Uparrow") { + repeat = bottom = "\u2016"; + } else if (delim === "\\downarrow") { + top = repeat = "\u23d0"; + } else if (delim === "\\Downarrow") { + top = repeat = "\u2016"; + } else if (delim === "\\updownarrow") { + top = "\\uparrow"; + repeat = "\u23d0"; + bottom = "\\downarrow"; + } else if (delim === "\\Updownarrow") { + top = "\\Uparrow"; + repeat = "\u2016"; + bottom = "\\Downarrow"; + } else if (delim === "[" || delim === "\\lbrack") { + top = "\u23a1"; + repeat = "\u23a2"; + bottom = "\u23a3"; + font = "Size4-Regular"; + } else if (delim === "]" || delim === "\\rbrack") { + top = "\u23a4"; + repeat = "\u23a5"; + bottom = "\u23a6"; + font = "Size4-Regular"; + } else if (delim === "\\lfloor") { + repeat = top = "\u23a2"; + bottom = "\u23a3"; + font = "Size4-Regular"; + } else if (delim === "\\lceil") { + top = "\u23a1"; + repeat = bottom = "\u23a2"; + font = "Size4-Regular"; + } else if (delim === "\\rfloor") { + repeat = top = "\u23a5"; + bottom = "\u23a6"; + font = "Size4-Regular"; + } else if (delim === "\\rceil") { + top = "\u23a4"; + repeat = bottom = "\u23a5"; + font = "Size4-Regular"; + } else if (delim === "(") { + top = "\u239b"; + repeat = "\u239c"; + bottom = "\u239d"; + font = "Size4-Regular"; + } else if (delim === ")") { + top = "\u239e"; + repeat = "\u239f"; + bottom = "\u23a0"; + font = "Size4-Regular"; + } else if (delim === "\\{" || delim === "\\lbrace") { + top = "\u23a7"; + middle = "\u23a8"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\}" || delim === "\\rbrace") { + top = "\u23ab"; + middle = "\u23ac"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\lgroup") { + top = "\u23a7"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\rgroup") { + top = "\u23ab"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\lmoustache") { + top = "\u23a7"; + bottom = "\u23ad"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\rmoustache") { + top = "\u23ab"; + bottom = "\u23a9"; + repeat = "\u23aa"; + font = "Size4-Regular"; + } else if (delim === "\\surd") { + top = "\ue001"; + bottom = "\u23b7"; + repeat = "\ue000"; + font = "Size4-Regular"; + } + + // Get the metrics of the four sections + var topMetrics = getMetrics(top, font); + var topHeightTotal = topMetrics.height + topMetrics.depth; + var repeatMetrics = getMetrics(repeat, font); + var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth; + var bottomMetrics = getMetrics(bottom, font); + var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth; + var middleHeightTotal = 0; + var middleFactor = 1; + if (middle !== null) { + var middleMetrics = getMetrics(middle, font); + middleHeightTotal = middleMetrics.height + middleMetrics.depth; + middleFactor = 2; // repeat symmetrically above and below middle + } + + // Calcuate the minimal height that the delimiter can have. + // It is at least the size of the top, bottom, and optional middle combined. + var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal; + + // Compute the number of copies of the repeat symbol we will need + var repeatCount = Math.ceil( + (heightTotal - minHeight) / (middleFactor * repeatHeightTotal)); + + // Compute the total height of the delimiter including all the symbols + var realHeightTotal = + minHeight + repeatCount * middleFactor * repeatHeightTotal; + + // The center of the delimiter is placed at the center of the axis. Note + // that in this context, "center" means that the delimiter should be + // centered around the axis in the current style, while normally it is + // centered around the axis in textstyle. + var axisHeight = fontMetrics.metrics.axisHeight; + if (center) { + axisHeight *= options.style.sizeMultiplier; + } + // Calculate the depth + var depth = realHeightTotal / 2 - axisHeight; + + // Now, we start building the pieces that will go into the vlist + + // Keep a list of the inner pieces + var inners = []; + + // Add the bottom symbol + inners.push(makeInner(bottom, font, mode)); + + var i; + if (middle === null) { + // Add that many symbols + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + } else { + // When there is a middle bit, we need the middle part and two repeated + // sections + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + inners.push(makeInner(middle, font, mode)); + for (i = 0; i < repeatCount; i++) { + inners.push(makeInner(repeat, font, mode)); + } + } + + // Add the top symbol + inners.push(makeInner(top, font, mode)); + + // Finally, build the vlist + var inner = buildCommon.makeVList(inners, "bottom", depth, options); + + return styleWrap( + makeSpan(["delimsizing", "mult"], [inner], options.getColor()), + Style.TEXT, options); +}; + +// There are three kinds of delimiters, delimiters that stack when they become +// too large +var stackLargeDelimiters = [ + "(", ")", "[", "\\lbrack", "]", "\\rbrack", + "\\{", "\\lbrace", "\\}", "\\rbrace", + "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", + "\\surd", +]; + +// delimiters that always stack +var stackAlwaysDelimiters = [ + "\\uparrow", "\\downarrow", "\\updownarrow", + "\\Uparrow", "\\Downarrow", "\\Updownarrow", + "|", "\\|", "\\vert", "\\Vert", + "\\lvert", "\\rvert", "\\lVert", "\\rVert", + "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", +]; + +// and delimiters that never stack +var stackNeverDelimiters = [ + "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt", +]; + +// Metrics of the different sizes. Found by looking at TeX's output of +// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ +// Used to create stacked delimiters of appropriate sizes in makeSizedDelim. +var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; + +/** + * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4. + */ +var makeSizedDelim = function(delim, size, options, mode) { + // < and > turn into \langle and \rangle in delimiters + if (delim === "<" || delim === "\\lt") { + delim = "\\langle"; + } else if (delim === ">" || delim === "\\gt") { + delim = "\\rangle"; + } + + // Sized delimiters are never centered. + if (utils.contains(stackLargeDelimiters, delim) || + utils.contains(stackNeverDelimiters, delim)) { + return makeLargeDelim(delim, size, false, options, mode); + } else if (utils.contains(stackAlwaysDelimiters, delim)) { + return makeStackedDelim( + delim, sizeToMaxHeight[size], false, options, mode); + } else { + throw new ParseError("Illegal delimiter: '" + delim + "'"); + } +}; + +/** + * There are three different sequences of delimiter sizes that the delimiters + * follow depending on the kind of delimiter. This is used when creating custom + * sized delimiters to decide whether to create a small, large, or stacked + * delimiter. + * + * In real TeX, these sequences aren't explicitly defined, but are instead + * defined inside the font metrics. Since there are only three sequences that + * are possible for the delimiters that TeX defines, it is easier to just encode + * them explicitly here. + */ + +// Delimiters that never stack try small delimiters and large delimiters only +var stackNeverDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "large", size: 1}, + {type: "large", size: 2}, + {type: "large", size: 3}, + {type: "large", size: 4}, +]; + +// Delimiters that always stack try the small delimiters first, then stack +var stackAlwaysDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "stack"}, +]; + +// Delimiters that stack when large try the small and then large delimiters, and +// stack afterwards +var stackLargeDelimiterSequence = [ + {type: "small", style: Style.SCRIPTSCRIPT}, + {type: "small", style: Style.SCRIPT}, + {type: "small", style: Style.TEXT}, + {type: "large", size: 1}, + {type: "large", size: 2}, + {type: "large", size: 3}, + {type: "large", size: 4}, + {type: "stack"}, +]; + +/** + * Get the font used in a delimiter based on what kind of delimiter it is. + */ +var delimTypeToFont = function(type) { + if (type.type === "small") { + return "Main-Regular"; + } else if (type.type === "large") { + return "Size" + type.size + "-Regular"; + } else if (type.type === "stack") { + return "Size4-Regular"; + } +}; + +/** + * Traverse a sequence of types of delimiters to decide what kind of delimiter + * should be used to create a delimiter of the given height+depth. + */ +var traverseSequence = function(delim, height, sequence, options) { + // Here, we choose the index we should start at in the sequences. In smaller + // sizes (which correspond to larger numbers in style.size) we start earlier + // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts + // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2 + var start = Math.min(2, 3 - options.style.size); + for (var i = start; i < sequence.length; i++) { + if (sequence[i].type === "stack") { + // This is always the last delimiter, so we just break the loop now. + break; + } + + var metrics = getMetrics(delim, delimTypeToFont(sequence[i])); + var heightDepth = metrics.height + metrics.depth; + + // Small delimiters are scaled down versions of the same font, so we + // account for the style change size. + + if (sequence[i].type === "small") { + heightDepth *= sequence[i].style.sizeMultiplier; + } + + // Check if the delimiter at this size works for the given height. + if (heightDepth > height) { + return sequence[i]; + } + } + + // If we reached the end of the sequence, return the last sequence element. + return sequence[sequence.length - 1]; +}; + +/** + * Make a delimiter of a given height+depth, with optional centering. Here, we + * traverse the sequences, and create a delimiter that the sequence tells us to. + */ +var makeCustomSizedDelim = function(delim, height, center, options, mode) { + if (delim === "<" || delim === "\\lt") { + delim = "\\langle"; + } else if (delim === ">" || delim === "\\gt") { + delim = "\\rangle"; + } + + // Decide what sequence to use + var sequence; + if (utils.contains(stackNeverDelimiters, delim)) { + sequence = stackNeverDelimiterSequence; + } else if (utils.contains(stackLargeDelimiters, delim)) { + sequence = stackLargeDelimiterSequence; + } else { + sequence = stackAlwaysDelimiterSequence; + } + + // Look through the sequence + var delimType = traverseSequence(delim, height, sequence, options); + + // Depending on the sequence element we decided on, call the appropriate + // function. + if (delimType.type === "small") { + return makeSmallDelim(delim, delimType.style, center, options, mode); + } else if (delimType.type === "large") { + return makeLargeDelim(delim, delimType.size, center, options, mode); + } else if (delimType.type === "stack") { + return makeStackedDelim(delim, height, center, options, mode); + } +}; + +/** + * Make a delimiter for use with `\left` and `\right`, given a height and depth + * of an expression that the delimiters surround. + */ +var makeLeftRightDelim = function(delim, height, depth, options, mode) { + // We always center \left/\right delimiters, so the axis is always shifted + var axisHeight = + fontMetrics.metrics.axisHeight * options.style.sizeMultiplier; + + // Taken from TeX source, tex.web, function make_left_right + var delimiterFactor = 901; + var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm; + + var maxDistFromAxis = Math.max( + height - axisHeight, depth + axisHeight); + + var totalHeight = Math.max( + // In real TeX, calculations are done using integral values which are + // 65536 per pt, or 655360 per em. So, the division here truncates in + // TeX but doesn't here, producing different results. If we wanted to + // exactly match TeX's calculation, we could do + // Math.floor(655360 * maxDistFromAxis / 500) * + // delimiterFactor / 655360 + // (To see the difference, compare + // x^{x^{\left(\rule{0.1em}{0.68em}\right)}} + // in TeX and KaTeX) + maxDistFromAxis / 500 * delimiterFactor, + 2 * maxDistFromAxis - delimiterExtend); + + // Finally, we defer to `makeCustomSizedDelim` with our calculated total + // height + return makeCustomSizedDelim(delim, totalHeight, true, options, mode); +}; + +module.exports = { + sizedDelim: makeSizedDelim, + customSizedDelim: makeCustomSizedDelim, + leftRightDelim: makeLeftRightDelim, +}; + +},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./fontMetrics":17,"./symbols":23,"./utils":25}],15:[function(require,module,exports){ +/** + * These objects store the data about the DOM nodes we create, as well as some + * extra data. They can then be transformed into real DOM nodes with the + * `toNode` function or HTML markup using `toMarkup`. They are useful for both + * storing extra properties on the nodes, as well as providing a way to easily + * work with the DOM. + * + * Similar functions for working with MathML nodes exist in mathMLTree.js. + */ +var unicodeRegexes = require("./unicodeRegexes"); +var utils = require("./utils"); + +/** + * Create an HTML className based on a list of classes. In addition to joining + * with spaces, we also remove null or empty classes. + */ +var createClass = function(classes) { + classes = classes.slice(); + for (var i = classes.length - 1; i >= 0; i--) { + if (!classes[i]) { + classes.splice(i, 1); + } + } + + return classes.join(" "); +}; + +/** + * This node represents a span node, with a className, a list of children, and + * an inline style. It also contains information about its height, depth, and + * maxFontSize. + */ +function span(classes, children, height, depth, maxFontSize, style) { + this.classes = classes || []; + this.children = children || []; + this.height = height || 0; + this.depth = depth || 0; + this.maxFontSize = maxFontSize || 0; + this.style = style || {}; + this.attributes = {}; +} + +/** + * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all + * browsers support attributes the same, and having too many custom attributes + * is probably bad. + */ +span.prototype.setAttribute = function(attribute, value) { + this.attributes[attribute] = value; +}; + +/** + * Convert the span into an HTML node + */ +span.prototype.toNode = function() { + var span = document.createElement("span"); + + // Apply the class + span.className = createClass(this.classes); + + // Apply inline styles + for (var style in this.style) { + if (Object.prototype.hasOwnProperty.call(this.style, style)) { + span.style[style] = this.style[style]; + } + } + + // Apply attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + span.setAttribute(attr, this.attributes[attr]); + } + } + + // Append the children, also as HTML nodes + for (var i = 0; i < this.children.length; i++) { + span.appendChild(this.children[i].toNode()); + } + + return span; +}; + +/** + * Convert the span into an HTML markup string + */ +span.prototype.toMarkup = function() { + var markup = "<span"; + + // Add the class + if (this.classes.length) { + markup += " class=\""; + markup += utils.escape(createClass(this.classes)); + markup += "\""; + } + + var styles = ""; + + // Add the styles, after hyphenation + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + markup += " style=\"" + utils.escape(styles) + "\""; + } + + // Add the attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + // Add the markup of the children, also as markup + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += "</span>"; + + return markup; +}; + +/** + * This node represents a document fragment, which contains elements, but when + * placed into the DOM doesn't have any representation itself. Thus, it only + * contains children and doesn't have any HTML properties. It also keeps track + * of a height, depth, and maxFontSize. + */ +function documentFragment(children, height, depth, maxFontSize) { + this.children = children || []; + this.height = height || 0; + this.depth = depth || 0; + this.maxFontSize = maxFontSize || 0; +} + +/** + * Convert the fragment into a node + */ +documentFragment.prototype.toNode = function() { + // Create a fragment + var frag = document.createDocumentFragment(); + + // Append the children + for (var i = 0; i < this.children.length; i++) { + frag.appendChild(this.children[i].toNode()); + } + + return frag; +}; + +/** + * Convert the fragment into HTML markup + */ +documentFragment.prototype.toMarkup = function() { + var markup = ""; + + // Simply concatenate the markup for the children together + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + return markup; +}; + +var iCombinations = { + 'î': '\u0131\u0302', + 'ï': '\u0131\u0308', + 'Ã': '\u0131\u0301', + // 'Ä«': '\u0131\u0304', // enable when we add Extended Latin + 'ì': '\u0131\u0300', +}; + +/** + * A symbol node contains information about a single symbol. It either renders + * to a single text node, or a span with a single text node in it, depending on + * whether it has CSS classes, styles, or needs italic correction. + */ +function symbolNode(value, height, depth, italic, skew, classes, style) { + this.value = value || ""; + this.height = height || 0; + this.depth = depth || 0; + this.italic = italic || 0; + this.skew = skew || 0; + this.classes = classes || []; + this.style = style || {}; + this.maxFontSize = 0; + + // Mark CJK characters with specific classes so that we can specify which + // fonts to use. This allows us to render these characters with a serif + // font in situations where the browser would either default to a sans serif + // or render a placeholder character. + if (unicodeRegexes.cjkRegex.test(value)) { + // I couldn't find any fonts that contained Hangul as well as all of + // the other characters we wanted to test there for it gets its own + // CSS class. + if (unicodeRegexes.hangulRegex.test(value)) { + this.classes.push('hangul_fallback'); + } else { + this.classes.push('cjk_fallback'); + } + } + + if (/[îïÃì]/.test(this.value)) { // add Ä« when we add Extended Latin + this.value = iCombinations[this.value]; + } +} + +/** + * Creates a text node or span from a symbol node. Note that a span is only + * created if it is needed. + */ +symbolNode.prototype.toNode = function() { + var node = document.createTextNode(this.value); + var span = null; + + if (this.italic > 0) { + span = document.createElement("span"); + span.style.marginRight = this.italic + "em"; + } + + if (this.classes.length > 0) { + span = span || document.createElement("span"); + span.className = createClass(this.classes); + } + + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + span = span || document.createElement("span"); + span.style[style] = this.style[style]; + } + } + + if (span) { + span.appendChild(node); + return span; + } else { + return node; + } +}; + +/** + * Creates markup for a symbol node. + */ +symbolNode.prototype.toMarkup = function() { + // TODO(alpert): More duplication than I'd like from + // span.prototype.toMarkup and symbolNode.prototype.toNode... + var needsSpan = false; + + var markup = "<span"; + + if (this.classes.length) { + needsSpan = true; + markup += " class=\""; + markup += utils.escape(createClass(this.classes)); + markup += "\""; + } + + var styles = ""; + + if (this.italic > 0) { + styles += "margin-right:" + this.italic + "em;"; + } + for (var style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + needsSpan = true; + markup += " style=\"" + utils.escape(styles) + "\""; + } + + var escaped = utils.escape(this.value); + if (needsSpan) { + markup += ">"; + markup += escaped; + markup += "</span>"; + return markup; + } else { + return escaped; + } +}; + +module.exports = { + span: span, + documentFragment: documentFragment, + symbolNode: symbolNode, +}; + +},{"./unicodeRegexes":24,"./utils":25}],16:[function(require,module,exports){ +/* eslint no-constant-condition:0 */ +var fontMetrics = require("./fontMetrics"); +var parseData = require("./parseData"); +var ParseError = require("./ParseError"); + +var ParseNode = parseData.ParseNode; + +/** + * Parse the body of the environment, with rows delimited by \\ and + * columns delimited by &, and create a nested list in row-major order + * with one group per cell. + */ +function parseArray(parser, result) { + var row = []; + var body = [row]; + var rowGaps = []; + while (true) { + var cell = parser.parseExpression(false, null); + row.push(new ParseNode("ordgroup", cell, parser.mode)); + var next = parser.nextToken.text; + if (next === "&") { + parser.consume(); + } else if (next === "\\end") { + break; + } else if (next === "\\\\" || next === "\\cr") { + var cr = parser.parseFunction(); + rowGaps.push(cr.value.size); + row = []; + body.push(row); + } else { + throw new ParseError("Expected & or \\\\ or \\end", + parser.nextToken); + } + } + result.body = body; + result.rowGaps = rowGaps; + return new ParseNode(result.type, result, parser.mode); +} + +/* + * An environment definition is very similar to a function definition: + * it is declared with a name or a list of names, a set of properties + * and a handler containing the actual implementation. + * + * The properties include: + * - numArgs: The number of arguments after the \begin{name} function. + * - argTypes: (optional) Just like for a function + * - allowedInText: (optional) Whether or not the environment is allowed inside + * text mode (default false) (not enforced yet) + * - numOptionalArgs: (optional) Just like for a function + * A bare number instead of that object indicates the numArgs value. + * + * The handler function will receive two arguments + * - context: information and references provided by the parser + * - args: an array of arguments passed to \begin{name} + * The context contains the following properties: + * - envName: the name of the environment, one of the listed names. + * - parser: the parser object + * - lexer: the lexer object + * - positions: the positions associated with these arguments from args. + * The handler must return a ParseResult. + */ + +function defineEnvironment(names, props, handler) { + if (typeof names === "string") { + names = [names]; + } + if (typeof props === "number") { + props = { numArgs: props }; + } + // Set default values of environments + var data = { + numArgs: props.numArgs || 0, + argTypes: props.argTypes, + greediness: 1, + allowedInText: !!props.allowedInText, + numOptionalArgs: props.numOptionalArgs || 0, + handler: handler, + }; + for (var i = 0; i < names.length; ++i) { + module.exports[names[i]] = data; + } +} + +// Arrays are part of LaTeX, defined in lttab.dtx so its documentation +// is part of the source2e.pdf file of LaTeX2e source documentation. +defineEnvironment("array", { + numArgs: 1, +}, function(context, args) { + var colalign = args[0]; + colalign = colalign.value.map ? colalign.value : [colalign]; + var cols = colalign.map(function(node) { + var ca = node.value; + if ("lcr".indexOf(ca) !== -1) { + return { + type: "align", + align: ca, + }; + } else if (ca === "|") { + return { + type: "separator", + separator: "|", + }; + } + throw new ParseError( + "Unknown column alignment: " + node.value, + node); + }); + var res = { + type: "array", + cols: cols, + hskipBeforeAndAfter: true, // \@preamble in lttab.dtx + }; + res = parseArray(context.parser, res); + return res; +}); + +// The matrix environments of amsmath builds on the array environment +// of LaTeX, which is discussed above. +defineEnvironment([ + "matrix", + "pmatrix", + "bmatrix", + "Bmatrix", + "vmatrix", + "Vmatrix", +], { +}, function(context) { + var delimiters = { + "matrix": null, + "pmatrix": ["(", ")"], + "bmatrix": ["[", "]"], + "Bmatrix": ["\\{", "\\}"], + "vmatrix": ["|", "|"], + "Vmatrix": ["\\Vert", "\\Vert"], + }[context.envName]; + var res = { + type: "array", + hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath + }; + res = parseArray(context.parser, res); + if (delimiters) { + res = new ParseNode("leftright", { + body: [res], + left: delimiters[0], + right: delimiters[1], + }, context.mode); + } + return res; +}); + +// A cases environment (in amsmath.sty) is almost equivalent to +// \def\arraystretch{1.2}% +// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. +defineEnvironment("cases", { +}, function(context) { + var res = { + type: "array", + arraystretch: 1.2, + cols: [{ + type: "align", + align: "l", + pregap: 0, + postgap: fontMetrics.metrics.quad, + }, { + type: "align", + align: "l", + pregap: 0, + postgap: 0, + }], + }; + res = parseArray(context.parser, res); + res = new ParseNode("leftright", { + body: [res], + left: "\\{", + right: ".", + }, context.mode); + return res; +}); + +// An aligned environment is like the align* environment +// except it operates within math mode. +// Note that we assume \nomallineskiplimit to be zero, +// so that \strut@ is the same as \strut. +defineEnvironment("aligned", { +}, function(context) { + var res = { + type: "array", + cols: [], + }; + res = parseArray(context.parser, res); + var emptyGroup = new ParseNode("ordgroup", [], context.mode); + var numCols = 0; + res.value.body.forEach(function(row) { + var i; + for (i = 1; i < row.length; i += 2) { + row[i].value.unshift(emptyGroup); + } + if (numCols < row.length) { + numCols = row.length; + } + }); + for (var i = 0; i < numCols; ++i) { + var align = "r"; + var pregap = 0; + if (i % 2 === 1) { + align = "l"; + } else if (i > 0) { + pregap = 2; // one \qquad between columns + } + res.value.cols[i] = { + type: "align", + align: align, + pregap: pregap, + postgap: 0, + }; + } + return res; +}); + +},{"./ParseError":6,"./fontMetrics":17,"./parseData":21}],17:[function(require,module,exports){ +/* eslint no-unused-vars:0 */ + +var Style = require("./Style"); +var cjkRegex = require("./unicodeRegexes").cjkRegex; + +/** + * This file contains metrics regarding fonts and individual symbols. The sigma + * and xi variables, as well as the metricMap map contain data extracted from + * TeX, TeX font metrics, and the TTF files. These data are then exposed via the + * `metrics` variable and the getCharacterMetrics function. + */ + +// These font metrics are extracted from TeX by using +// \font\a=cmmi10 +// \showthe\fontdimenX\a +// where X is the corresponding variable number. These correspond to the font +// parameters of the symbol fonts. In TeX, there are actually three sets of +// dimensions, one for each of textstyle, scriptstyle, and scriptscriptstyle, +// but we only use the textstyle ones, and scale certain dimensions accordingly. +// See the TeXbook, page 441. +var sigma1 = 0.025; +var sigma2 = 0; +var sigma3 = 0; +var sigma4 = 0; +var sigma5 = 0.431; +var sigma6 = 1; +var sigma7 = 0; +var sigma8 = 0.677; +var sigma9 = 0.394; +var sigma10 = 0.444; +var sigma11 = 0.686; +var sigma12 = 0.345; +var sigma13 = 0.413; +var sigma14 = 0.363; +var sigma15 = 0.289; +var sigma16 = 0.150; +var sigma17 = 0.247; +var sigma18 = 0.386; +var sigma19 = 0.050; +var sigma20 = 2.390; +var sigma21 = 1.01; +var sigma21Script = 0.81; +var sigma21ScriptScript = 0.71; +var sigma22 = 0.250; + +// These font metrics are extracted from TeX by using +// \font\a=cmex10 +// \showthe\fontdimenX\a +// where X is the corresponding variable number. These correspond to the font +// parameters of the extension fonts (family 3). See the TeXbook, page 441. +var xi1 = 0; +var xi2 = 0; +var xi3 = 0; +var xi4 = 0; +var xi5 = 0.431; +var xi6 = 1; +var xi7 = 0; +var xi8 = 0.04; +var xi9 = 0.111; +var xi10 = 0.166; +var xi11 = 0.2; +var xi12 = 0.6; +var xi13 = 0.1; + +// This value determines how large a pt is, for metrics which are defined in +// terms of pts. +// This value is also used in katex.less; if you change it make sure the values +// match. +var ptPerEm = 10.0; + +// The space between adjacent `|` columns in an array definition. From +// `\showthe\doublerulesep` in LaTeX. +var doubleRuleSep = 2.0 / ptPerEm; + +/** + * This is just a mapping from common names to real metrics + */ +var metrics = { + xHeight: sigma5, + quad: sigma6, + num1: sigma8, + num2: sigma9, + num3: sigma10, + denom1: sigma11, + denom2: sigma12, + sup1: sigma13, + sup2: sigma14, + sup3: sigma15, + sub1: sigma16, + sub2: sigma17, + supDrop: sigma18, + subDrop: sigma19, + axisHeight: sigma22, + defaultRuleThickness: xi8, + bigOpSpacing1: xi9, + bigOpSpacing2: xi10, + bigOpSpacing3: xi11, + bigOpSpacing4: xi12, + bigOpSpacing5: xi13, + ptPerEm: ptPerEm, + emPerEx: sigma5 / sigma6, + doubleRuleSep: doubleRuleSep, + + // TODO(alpert): Missing parallel structure here. We should probably add + // style-specific metrics for all of these. + delim1: sigma20, + getDelim2: function(style) { + if (style.size === Style.TEXT.size) { + return sigma21; + } else if (style.size === Style.SCRIPT.size) { + return sigma21Script; + } else if (style.size === Style.SCRIPTSCRIPT.size) { + return sigma21ScriptScript; + } + throw new Error("Unexpected style size: " + style.size); + }, +}; + +// This map contains a mapping from font name and character code to character +// metrics, including height, depth, italic correction, and skew (kern from the +// character to the corresponding \skewchar) +// This map is generated via `make metrics`. It should not be changed manually. +var metricMap = require("./fontMetricsData"); + +// These are very rough approximations. We default to Times New Roman which +// should have Latin-1 and Cyrillic characters, but may not depending on the +// operating system. The metrics do not account for extra height from the +// accents. In the case of Cyrillic characters which have both ascenders and +// descenders we prefer approximations with ascenders, primarily to prevent +// the fraction bar or root line from intersecting the glyph. +// TODO(kevinb) allow union of multiple glyph metrics for better accuracy. +var extraCharacterMap = { + // Latin-1 + 'À': 'A', + 'Ã': 'A', + 'Â': 'A', + 'Ã': 'A', + 'Ä': 'A', + 'Ã…': 'A', + 'Æ': 'A', + 'Ç': 'C', + 'È': 'E', + 'É': 'E', + 'Ê': 'E', + 'Ë': 'E', + 'ÃŒ': 'I', + 'Ã': 'I', + 'ÃŽ': 'I', + 'Ã': 'I', + 'Ã': 'D', + 'Ñ': 'N', + 'Ã’': 'O', + 'Ó': 'O', + 'Ô': 'O', + 'Õ': 'O', + 'Ö': 'O', + 'Ø': 'O', + 'Ù': 'U', + 'Ú': 'U', + 'Û': 'U', + 'Ãœ': 'U', + 'Ã': 'Y', + 'Þ': 'o', + 'ß': 'B', + 'à ': 'a', + 'á': 'a', + 'â': 'a', + 'ã': 'a', + 'ä': 'a', + 'Ã¥': 'a', + 'æ': 'a', + 'ç': 'c', + 'è': 'e', + 'é': 'e', + 'ê': 'e', + 'ë': 'e', + 'ì': 'i', + 'Ã': 'i', + 'î': 'i', + 'ï': 'i', + 'ð': 'd', + 'ñ': 'n', + 'ò': 'o', + 'ó': 'o', + 'ô': 'o', + 'õ': 'o', + 'ö': 'o', + 'ø': 'o', + 'ù': 'u', + 'ú': 'u', + 'û': 'u', + 'ü': 'u', + 'ý': 'y', + 'þ': 'o', + 'ÿ': 'y', + + // Cyrillic + 'Ð': 'A', + 'Б': 'B', + 'Ð’': 'B', + 'Г': 'F', + 'Д': 'A', + 'Е': 'E', + 'Ж': 'K', + 'З': '3', + 'И': 'N', + 'Й': 'N', + 'К': 'K', + 'Л': 'N', + 'Ðœ': 'M', + 'Ð': 'H', + 'О': 'O', + 'П': 'N', + 'Ð ': 'P', + 'С': 'C', + 'Т': 'T', + 'У': 'y', + 'Ф': 'O', + 'Ð¥': 'X', + 'Ц': 'U', + 'Ч': 'h', + 'Ш': 'W', + 'Щ': 'W', + 'Ъ': 'B', + 'Ы': 'X', + 'Ь': 'B', + 'Ð': '3', + 'Ю': 'X', + 'Я': 'R', + 'а': 'a', + 'б': 'b', + 'в': 'a', + 'г': 'r', + 'д': 'y', + 'е': 'e', + 'ж': 'm', + 'з': 'e', + 'и': 'n', + 'й': 'n', + 'к': 'n', + 'л': 'n', + 'м': 'm', + 'н': 'n', + 'о': 'o', + 'п': 'n', + 'Ñ€': 'p', + 'Ñ': 'c', + 'Ñ‚': 'o', + 'у': 'y', + 'Ñ„': 'b', + 'Ñ…': 'x', + 'ц': 'n', + 'ч': 'n', + 'ш': 'w', + 'щ': 'w', + 'ÑŠ': 'a', + 'Ñ‹': 'm', + 'ÑŒ': 'a', + 'Ñ': 'e', + 'ÑŽ': 'm', + 'Ñ': 'r', +}; + +/** + * This function is a convenience function for looking up information in the + * metricMap table. It takes a character as a string, and a style. + * + * Note: the `width` property may be undefined if fontMetricsData.js wasn't + * built using `Make extended_metrics`. + */ +var getCharacterMetrics = function(character, style) { + var ch = character.charCodeAt(0); + if (character[0] in extraCharacterMap) { + ch = extraCharacterMap[character[0]].charCodeAt(0); + } else if (cjkRegex.test(character[0])) { + ch = 'M'.charCodeAt(0); + } + var metrics = metricMap[style][ch]; + if (metrics) { + return { + depth: metrics[0], + height: metrics[1], + italic: metrics[2], + skew: metrics[3], + width: metrics[4], + }; + } +}; + +module.exports = { + metrics: metrics, + getCharacterMetrics: getCharacterMetrics, +}; + +},{"./Style":9,"./fontMetricsData":18,"./unicodeRegexes":24}],18:[function(require,module,exports){ +module.exports = { + "AMS-Regular": { + "65": [0, 0.68889, 0, 0], + "66": [0, 0.68889, 0, 0], + "67": [0, 0.68889, 0, 0], + "68": [0, 0.68889, 0, 0], + "69": [0, 0.68889, 0, 0], + "70": [0, 0.68889, 0, 0], + "71": [0, 0.68889, 0, 0], + "72": [0, 0.68889, 0, 0], + "73": [0, 0.68889, 0, 0], + "74": [0.16667, 0.68889, 0, 0], + "75": [0, 0.68889, 0, 0], + "76": [0, 0.68889, 0, 0], + "77": [0, 0.68889, 0, 0], + "78": [0, 0.68889, 0, 0], + "79": [0.16667, 0.68889, 0, 0], + "80": [0, 0.68889, 0, 0], + "81": [0.16667, 0.68889, 0, 0], + "82": [0, 0.68889, 0, 0], + "83": [0, 0.68889, 0, 0], + "84": [0, 0.68889, 0, 0], + "85": [0, 0.68889, 0, 0], + "86": [0, 0.68889, 0, 0], + "87": [0, 0.68889, 0, 0], + "88": [0, 0.68889, 0, 0], + "89": [0, 0.68889, 0, 0], + "90": [0, 0.68889, 0, 0], + "107": [0, 0.68889, 0, 0], + "165": [0, 0.675, 0.025, 0], + "174": [0.15559, 0.69224, 0, 0], + "240": [0, 0.68889, 0, 0], + "295": [0, 0.68889, 0, 0], + "710": [0, 0.825, 0, 0], + "732": [0, 0.9, 0, 0], + "770": [0, 0.825, 0, 0], + "771": [0, 0.9, 0, 0], + "989": [0.08167, 0.58167, 0, 0], + "1008": [0, 0.43056, 0.04028, 0], + "8245": [0, 0.54986, 0, 0], + "8463": [0, 0.68889, 0, 0], + "8487": [0, 0.68889, 0, 0], + "8498": [0, 0.68889, 0, 0], + "8502": [0, 0.68889, 0, 0], + "8503": [0, 0.68889, 0, 0], + "8504": [0, 0.68889, 0, 0], + "8513": [0, 0.68889, 0, 0], + "8592": [-0.03598, 0.46402, 0, 0], + "8594": [-0.03598, 0.46402, 0, 0], + "8602": [-0.13313, 0.36687, 0, 0], + "8603": [-0.13313, 0.36687, 0, 0], + "8606": [0.01354, 0.52239, 0, 0], + "8608": [0.01354, 0.52239, 0, 0], + "8610": [0.01354, 0.52239, 0, 0], + "8611": [0.01354, 0.52239, 0, 0], + "8619": [0, 0.54986, 0, 0], + "8620": [0, 0.54986, 0, 0], + "8621": [-0.13313, 0.37788, 0, 0], + "8622": [-0.13313, 0.36687, 0, 0], + "8624": [0, 0.69224, 0, 0], + "8625": [0, 0.69224, 0, 0], + "8630": [0, 0.43056, 0, 0], + "8631": [0, 0.43056, 0, 0], + "8634": [0.08198, 0.58198, 0, 0], + "8635": [0.08198, 0.58198, 0, 0], + "8638": [0.19444, 0.69224, 0, 0], + "8639": [0.19444, 0.69224, 0, 0], + "8642": [0.19444, 0.69224, 0, 0], + "8643": [0.19444, 0.69224, 0, 0], + "8644": [0.1808, 0.675, 0, 0], + "8646": [0.1808, 0.675, 0, 0], + "8647": [0.1808, 0.675, 0, 0], + "8648": [0.19444, 0.69224, 0, 0], + "8649": [0.1808, 0.675, 0, 0], + "8650": [0.19444, 0.69224, 0, 0], + "8651": [0.01354, 0.52239, 0, 0], + "8652": [0.01354, 0.52239, 0, 0], + "8653": [-0.13313, 0.36687, 0, 0], + "8654": [-0.13313, 0.36687, 0, 0], + "8655": [-0.13313, 0.36687, 0, 0], + "8666": [0.13667, 0.63667, 0, 0], + "8667": [0.13667, 0.63667, 0, 0], + "8669": [-0.13313, 0.37788, 0, 0], + "8672": [-0.064, 0.437, 0, 0], + "8674": [-0.064, 0.437, 0, 0], + "8705": [0, 0.825, 0, 0], + "8708": [0, 0.68889, 0, 0], + "8709": [0.08167, 0.58167, 0, 0], + "8717": [0, 0.43056, 0, 0], + "8722": [-0.03598, 0.46402, 0, 0], + "8724": [0.08198, 0.69224, 0, 0], + "8726": [0.08167, 0.58167, 0, 0], + "8733": [0, 0.69224, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8737": [0, 0.69224, 0, 0], + "8738": [0.03517, 0.52239, 0, 0], + "8739": [0.08167, 0.58167, 0, 0], + "8740": [0.25142, 0.74111, 0, 0], + "8741": [0.08167, 0.58167, 0, 0], + "8742": [0.25142, 0.74111, 0, 0], + "8756": [0, 0.69224, 0, 0], + "8757": [0, 0.69224, 0, 0], + "8764": [-0.13313, 0.36687, 0, 0], + "8765": [-0.13313, 0.37788, 0, 0], + "8769": [-0.13313, 0.36687, 0, 0], + "8770": [-0.03625, 0.46375, 0, 0], + "8774": [0.30274, 0.79383, 0, 0], + "8776": [-0.01688, 0.48312, 0, 0], + "8778": [0.08167, 0.58167, 0, 0], + "8782": [0.06062, 0.54986, 0, 0], + "8783": [0.06062, 0.54986, 0, 0], + "8785": [0.08198, 0.58198, 0, 0], + "8786": [0.08198, 0.58198, 0, 0], + "8787": [0.08198, 0.58198, 0, 0], + "8790": [0, 0.69224, 0, 0], + "8791": [0.22958, 0.72958, 0, 0], + "8796": [0.08198, 0.91667, 0, 0], + "8806": [0.25583, 0.75583, 0, 0], + "8807": [0.25583, 0.75583, 0, 0], + "8808": [0.25142, 0.75726, 0, 0], + "8809": [0.25142, 0.75726, 0, 0], + "8812": [0.25583, 0.75583, 0, 0], + "8814": [0.20576, 0.70576, 0, 0], + "8815": [0.20576, 0.70576, 0, 0], + "8816": [0.30274, 0.79383, 0, 0], + "8817": [0.30274, 0.79383, 0, 0], + "8818": [0.22958, 0.72958, 0, 0], + "8819": [0.22958, 0.72958, 0, 0], + "8822": [0.1808, 0.675, 0, 0], + "8823": [0.1808, 0.675, 0, 0], + "8828": [0.13667, 0.63667, 0, 0], + "8829": [0.13667, 0.63667, 0, 0], + "8830": [0.22958, 0.72958, 0, 0], + "8831": [0.22958, 0.72958, 0, 0], + "8832": [0.20576, 0.70576, 0, 0], + "8833": [0.20576, 0.70576, 0, 0], + "8840": [0.30274, 0.79383, 0, 0], + "8841": [0.30274, 0.79383, 0, 0], + "8842": [0.13597, 0.63597, 0, 0], + "8843": [0.13597, 0.63597, 0, 0], + "8847": [0.03517, 0.54986, 0, 0], + "8848": [0.03517, 0.54986, 0, 0], + "8858": [0.08198, 0.58198, 0, 0], + "8859": [0.08198, 0.58198, 0, 0], + "8861": [0.08198, 0.58198, 0, 0], + "8862": [0, 0.675, 0, 0], + "8863": [0, 0.675, 0, 0], + "8864": [0, 0.675, 0, 0], + "8865": [0, 0.675, 0, 0], + "8872": [0, 0.69224, 0, 0], + "8873": [0, 0.69224, 0, 0], + "8874": [0, 0.69224, 0, 0], + "8876": [0, 0.68889, 0, 0], + "8877": [0, 0.68889, 0, 0], + "8878": [0, 0.68889, 0, 0], + "8879": [0, 0.68889, 0, 0], + "8882": [0.03517, 0.54986, 0, 0], + "8883": [0.03517, 0.54986, 0, 0], + "8884": [0.13667, 0.63667, 0, 0], + "8885": [0.13667, 0.63667, 0, 0], + "8888": [0, 0.54986, 0, 0], + "8890": [0.19444, 0.43056, 0, 0], + "8891": [0.19444, 0.69224, 0, 0], + "8892": [0.19444, 0.69224, 0, 0], + "8901": [0, 0.54986, 0, 0], + "8903": [0.08167, 0.58167, 0, 0], + "8905": [0.08167, 0.58167, 0, 0], + "8906": [0.08167, 0.58167, 0, 0], + "8907": [0, 0.69224, 0, 0], + "8908": [0, 0.69224, 0, 0], + "8909": [-0.03598, 0.46402, 0, 0], + "8910": [0, 0.54986, 0, 0], + "8911": [0, 0.54986, 0, 0], + "8912": [0.03517, 0.54986, 0, 0], + "8913": [0.03517, 0.54986, 0, 0], + "8914": [0, 0.54986, 0, 0], + "8915": [0, 0.54986, 0, 0], + "8916": [0, 0.69224, 0, 0], + "8918": [0.0391, 0.5391, 0, 0], + "8919": [0.0391, 0.5391, 0, 0], + "8920": [0.03517, 0.54986, 0, 0], + "8921": [0.03517, 0.54986, 0, 0], + "8922": [0.38569, 0.88569, 0, 0], + "8923": [0.38569, 0.88569, 0, 0], + "8926": [0.13667, 0.63667, 0, 0], + "8927": [0.13667, 0.63667, 0, 0], + "8928": [0.30274, 0.79383, 0, 0], + "8929": [0.30274, 0.79383, 0, 0], + "8934": [0.23222, 0.74111, 0, 0], + "8935": [0.23222, 0.74111, 0, 0], + "8936": [0.23222, 0.74111, 0, 0], + "8937": [0.23222, 0.74111, 0, 0], + "8938": [0.20576, 0.70576, 0, 0], + "8939": [0.20576, 0.70576, 0, 0], + "8940": [0.30274, 0.79383, 0, 0], + "8941": [0.30274, 0.79383, 0, 0], + "8994": [0.19444, 0.69224, 0, 0], + "8995": [0.19444, 0.69224, 0, 0], + "9416": [0.15559, 0.69224, 0, 0], + "9484": [0, 0.69224, 0, 0], + "9488": [0, 0.69224, 0, 0], + "9492": [0, 0.37788, 0, 0], + "9496": [0, 0.37788, 0, 0], + "9585": [0.19444, 0.68889, 0, 0], + "9586": [0.19444, 0.74111, 0, 0], + "9632": [0, 0.675, 0, 0], + "9633": [0, 0.675, 0, 0], + "9650": [0, 0.54986, 0, 0], + "9651": [0, 0.54986, 0, 0], + "9654": [0.03517, 0.54986, 0, 0], + "9660": [0, 0.54986, 0, 0], + "9661": [0, 0.54986, 0, 0], + "9664": [0.03517, 0.54986, 0, 0], + "9674": [0.11111, 0.69224, 0, 0], + "9733": [0.19444, 0.69224, 0, 0], + "10003": [0, 0.69224, 0, 0], + "10016": [0, 0.69224, 0, 0], + "10731": [0.11111, 0.69224, 0, 0], + "10846": [0.19444, 0.75583, 0, 0], + "10877": [0.13667, 0.63667, 0, 0], + "10878": [0.13667, 0.63667, 0, 0], + "10885": [0.25583, 0.75583, 0, 0], + "10886": [0.25583, 0.75583, 0, 0], + "10887": [0.13597, 0.63597, 0, 0], + "10888": [0.13597, 0.63597, 0, 0], + "10889": [0.26167, 0.75726, 0, 0], + "10890": [0.26167, 0.75726, 0, 0], + "10891": [0.48256, 0.98256, 0, 0], + "10892": [0.48256, 0.98256, 0, 0], + "10901": [0.13667, 0.63667, 0, 0], + "10902": [0.13667, 0.63667, 0, 0], + "10933": [0.25142, 0.75726, 0, 0], + "10934": [0.25142, 0.75726, 0, 0], + "10935": [0.26167, 0.75726, 0, 0], + "10936": [0.26167, 0.75726, 0, 0], + "10937": [0.26167, 0.75726, 0, 0], + "10938": [0.26167, 0.75726, 0, 0], + "10949": [0.25583, 0.75583, 0, 0], + "10950": [0.25583, 0.75583, 0, 0], + "10955": [0.28481, 0.79383, 0, 0], + "10956": [0.28481, 0.79383, 0, 0], + "57350": [0.08167, 0.58167, 0, 0], + "57351": [0.08167, 0.58167, 0, 0], + "57352": [0.08167, 0.58167, 0, 0], + "57353": [0, 0.43056, 0.04028, 0], + "57356": [0.25142, 0.75726, 0, 0], + "57357": [0.25142, 0.75726, 0, 0], + "57358": [0.41951, 0.91951, 0, 0], + "57359": [0.30274, 0.79383, 0, 0], + "57360": [0.30274, 0.79383, 0, 0], + "57361": [0.41951, 0.91951, 0, 0], + "57366": [0.25142, 0.75726, 0, 0], + "57367": [0.25142, 0.75726, 0, 0], + "57368": [0.25142, 0.75726, 0, 0], + "57369": [0.25142, 0.75726, 0, 0], + "57370": [0.13597, 0.63597, 0, 0], + "57371": [0.13597, 0.63597, 0, 0], + }, + "Caligraphic-Regular": { + "48": [0, 0.43056, 0, 0], + "49": [0, 0.43056, 0, 0], + "50": [0, 0.43056, 0, 0], + "51": [0.19444, 0.43056, 0, 0], + "52": [0.19444, 0.43056, 0, 0], + "53": [0.19444, 0.43056, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0.19444, 0.43056, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0.19444, 0.43056, 0, 0], + "65": [0, 0.68333, 0, 0.19445], + "66": [0, 0.68333, 0.03041, 0.13889], + "67": [0, 0.68333, 0.05834, 0.13889], + "68": [0, 0.68333, 0.02778, 0.08334], + "69": [0, 0.68333, 0.08944, 0.11111], + "70": [0, 0.68333, 0.09931, 0.11111], + "71": [0.09722, 0.68333, 0.0593, 0.11111], + "72": [0, 0.68333, 0.00965, 0.11111], + "73": [0, 0.68333, 0.07382, 0], + "74": [0.09722, 0.68333, 0.18472, 0.16667], + "75": [0, 0.68333, 0.01445, 0.05556], + "76": [0, 0.68333, 0, 0.13889], + "77": [0, 0.68333, 0, 0.13889], + "78": [0, 0.68333, 0.14736, 0.08334], + "79": [0, 0.68333, 0.02778, 0.11111], + "80": [0, 0.68333, 0.08222, 0.08334], + "81": [0.09722, 0.68333, 0, 0.11111], + "82": [0, 0.68333, 0, 0.08334], + "83": [0, 0.68333, 0.075, 0.13889], + "84": [0, 0.68333, 0.25417, 0], + "85": [0, 0.68333, 0.09931, 0.08334], + "86": [0, 0.68333, 0.08222, 0], + "87": [0, 0.68333, 0.08222, 0.08334], + "88": [0, 0.68333, 0.14643, 0.13889], + "89": [0.09722, 0.68333, 0.08222, 0.08334], + "90": [0, 0.68333, 0.07944, 0.13889], + }, + "Fraktur-Regular": { + "33": [0, 0.69141, 0, 0], + "34": [0, 0.69141, 0, 0], + "38": [0, 0.69141, 0, 0], + "39": [0, 0.69141, 0, 0], + "40": [0.24982, 0.74947, 0, 0], + "41": [0.24982, 0.74947, 0, 0], + "42": [0, 0.62119, 0, 0], + "43": [0.08319, 0.58283, 0, 0], + "44": [0, 0.10803, 0, 0], + "45": [0.08319, 0.58283, 0, 0], + "46": [0, 0.10803, 0, 0], + "47": [0.24982, 0.74947, 0, 0], + "48": [0, 0.47534, 0, 0], + "49": [0, 0.47534, 0, 0], + "50": [0, 0.47534, 0, 0], + "51": [0.18906, 0.47534, 0, 0], + "52": [0.18906, 0.47534, 0, 0], + "53": [0.18906, 0.47534, 0, 0], + "54": [0, 0.69141, 0, 0], + "55": [0.18906, 0.47534, 0, 0], + "56": [0, 0.69141, 0, 0], + "57": [0.18906, 0.47534, 0, 0], + "58": [0, 0.47534, 0, 0], + "59": [0.12604, 0.47534, 0, 0], + "61": [-0.13099, 0.36866, 0, 0], + "63": [0, 0.69141, 0, 0], + "65": [0, 0.69141, 0, 0], + "66": [0, 0.69141, 0, 0], + "67": [0, 0.69141, 0, 0], + "68": [0, 0.69141, 0, 0], + "69": [0, 0.69141, 0, 0], + "70": [0.12604, 0.69141, 0, 0], + "71": [0, 0.69141, 0, 0], + "72": [0.06302, 0.69141, 0, 0], + "73": [0, 0.69141, 0, 0], + "74": [0.12604, 0.69141, 0, 0], + "75": [0, 0.69141, 0, 0], + "76": [0, 0.69141, 0, 0], + "77": [0, 0.69141, 0, 0], + "78": [0, 0.69141, 0, 0], + "79": [0, 0.69141, 0, 0], + "80": [0.18906, 0.69141, 0, 0], + "81": [0.03781, 0.69141, 0, 0], + "82": [0, 0.69141, 0, 0], + "83": [0, 0.69141, 0, 0], + "84": [0, 0.69141, 0, 0], + "85": [0, 0.69141, 0, 0], + "86": [0, 0.69141, 0, 0], + "87": [0, 0.69141, 0, 0], + "88": [0, 0.69141, 0, 0], + "89": [0.18906, 0.69141, 0, 0], + "90": [0.12604, 0.69141, 0, 0], + "91": [0.24982, 0.74947, 0, 0], + "93": [0.24982, 0.74947, 0, 0], + "94": [0, 0.69141, 0, 0], + "97": [0, 0.47534, 0, 0], + "98": [0, 0.69141, 0, 0], + "99": [0, 0.47534, 0, 0], + "100": [0, 0.62119, 0, 0], + "101": [0, 0.47534, 0, 0], + "102": [0.18906, 0.69141, 0, 0], + "103": [0.18906, 0.47534, 0, 0], + "104": [0.18906, 0.69141, 0, 0], + "105": [0, 0.69141, 0, 0], + "106": [0, 0.69141, 0, 0], + "107": [0, 0.69141, 0, 0], + "108": [0, 0.69141, 0, 0], + "109": [0, 0.47534, 0, 0], + "110": [0, 0.47534, 0, 0], + "111": [0, 0.47534, 0, 0], + "112": [0.18906, 0.52396, 0, 0], + "113": [0.18906, 0.47534, 0, 0], + "114": [0, 0.47534, 0, 0], + "115": [0, 0.47534, 0, 0], + "116": [0, 0.62119, 0, 0], + "117": [0, 0.47534, 0, 0], + "118": [0, 0.52396, 0, 0], + "119": [0, 0.52396, 0, 0], + "120": [0.18906, 0.47534, 0, 0], + "121": [0.18906, 0.47534, 0, 0], + "122": [0.18906, 0.47534, 0, 0], + "8216": [0, 0.69141, 0, 0], + "8217": [0, 0.69141, 0, 0], + "58112": [0, 0.62119, 0, 0], + "58113": [0, 0.62119, 0, 0], + "58114": [0.18906, 0.69141, 0, 0], + "58115": [0.18906, 0.69141, 0, 0], + "58116": [0.18906, 0.47534, 0, 0], + "58117": [0, 0.69141, 0, 0], + "58118": [0, 0.62119, 0, 0], + "58119": [0, 0.47534, 0, 0], + }, + "Main-Bold": { + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.13333, 0.63333, 0, 0], + "44": [0.19444, 0.15556, 0, 0], + "45": [0, 0.44444, 0, 0], + "46": [0, 0.15556, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.64444, 0, 0], + "49": [0, 0.64444, 0, 0], + "50": [0, 0.64444, 0, 0], + "51": [0, 0.64444, 0, 0], + "52": [0, 0.64444, 0, 0], + "53": [0, 0.64444, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0, 0.64444, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0, 0.64444, 0, 0], + "58": [0, 0.44444, 0, 0], + "59": [0.19444, 0.44444, 0, 0], + "60": [0.08556, 0.58556, 0, 0], + "61": [-0.10889, 0.39111, 0, 0], + "62": [0.08556, 0.58556, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.68611, 0, 0], + "66": [0, 0.68611, 0, 0], + "67": [0, 0.68611, 0, 0], + "68": [0, 0.68611, 0, 0], + "69": [0, 0.68611, 0, 0], + "70": [0, 0.68611, 0, 0], + "71": [0, 0.68611, 0, 0], + "72": [0, 0.68611, 0, 0], + "73": [0, 0.68611, 0, 0], + "74": [0, 0.68611, 0, 0], + "75": [0, 0.68611, 0, 0], + "76": [0, 0.68611, 0, 0], + "77": [0, 0.68611, 0, 0], + "78": [0, 0.68611, 0, 0], + "79": [0, 0.68611, 0, 0], + "80": [0, 0.68611, 0, 0], + "81": [0.19444, 0.68611, 0, 0], + "82": [0, 0.68611, 0, 0], + "83": [0, 0.68611, 0, 0], + "84": [0, 0.68611, 0, 0], + "85": [0, 0.68611, 0, 0], + "86": [0, 0.68611, 0.01597, 0], + "87": [0, 0.68611, 0.01597, 0], + "88": [0, 0.68611, 0, 0], + "89": [0, 0.68611, 0.02875, 0], + "90": [0, 0.68611, 0, 0], + "91": [0.25, 0.75, 0, 0], + "92": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.31, 0.13444, 0.03194, 0], + "96": [0, 0.69444, 0, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0, 0.69444, 0.10903, 0], + "103": [0.19444, 0.44444, 0.01597, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.69444, 0, 0], + "106": [0.19444, 0.69444, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0, 0], + "114": [0, 0.44444, 0, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.63492, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.01597, 0], + "119": [0, 0.44444, 0.01597, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.01597, 0], + "122": [0, 0.44444, 0, 0], + "123": [0.25, 0.75, 0, 0], + "124": [0.25, 0.75, 0, 0], + "125": [0.25, 0.75, 0, 0], + "126": [0.35, 0.34444, 0, 0], + "168": [0, 0.69444, 0, 0], + "172": [0, 0.44444, 0, 0], + "175": [0, 0.59611, 0, 0], + "176": [0, 0.69444, 0, 0], + "177": [0.13333, 0.63333, 0, 0], + "180": [0, 0.69444, 0, 0], + "215": [0.13333, 0.63333, 0, 0], + "247": [0.13333, 0.63333, 0, 0], + "305": [0, 0.44444, 0, 0], + "567": [0.19444, 0.44444, 0, 0], + "710": [0, 0.69444, 0, 0], + "711": [0, 0.63194, 0, 0], + "713": [0, 0.59611, 0, 0], + "714": [0, 0.69444, 0, 0], + "715": [0, 0.69444, 0, 0], + "728": [0, 0.69444, 0, 0], + "729": [0, 0.69444, 0, 0], + "730": [0, 0.69444, 0, 0], + "732": [0, 0.69444, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.69444, 0, 0], + "772": [0, 0.59611, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.69444, 0, 0], + "776": [0, 0.69444, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.63194, 0, 0], + "824": [0.19444, 0.69444, 0, 0], + "915": [0, 0.68611, 0, 0], + "916": [0, 0.68611, 0, 0], + "920": [0, 0.68611, 0, 0], + "923": [0, 0.68611, 0, 0], + "926": [0, 0.68611, 0, 0], + "928": [0, 0.68611, 0, 0], + "931": [0, 0.68611, 0, 0], + "933": [0, 0.68611, 0, 0], + "934": [0, 0.68611, 0, 0], + "936": [0, 0.68611, 0, 0], + "937": [0, 0.68611, 0, 0], + "8211": [0, 0.44444, 0.03194, 0], + "8212": [0, 0.44444, 0.03194, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + "8224": [0.19444, 0.69444, 0, 0], + "8225": [0.19444, 0.69444, 0, 0], + "8242": [0, 0.55556, 0, 0], + "8407": [0, 0.72444, 0.15486, 0], + "8463": [0, 0.69444, 0, 0], + "8465": [0, 0.69444, 0, 0], + "8467": [0, 0.69444, 0, 0], + "8472": [0.19444, 0.44444, 0, 0], + "8476": [0, 0.69444, 0, 0], + "8501": [0, 0.69444, 0, 0], + "8592": [-0.10889, 0.39111, 0, 0], + "8593": [0.19444, 0.69444, 0, 0], + "8594": [-0.10889, 0.39111, 0, 0], + "8595": [0.19444, 0.69444, 0, 0], + "8596": [-0.10889, 0.39111, 0, 0], + "8597": [0.25, 0.75, 0, 0], + "8598": [0.19444, 0.69444, 0, 0], + "8599": [0.19444, 0.69444, 0, 0], + "8600": [0.19444, 0.69444, 0, 0], + "8601": [0.19444, 0.69444, 0, 0], + "8636": [-0.10889, 0.39111, 0, 0], + "8637": [-0.10889, 0.39111, 0, 0], + "8640": [-0.10889, 0.39111, 0, 0], + "8641": [-0.10889, 0.39111, 0, 0], + "8656": [-0.10889, 0.39111, 0, 0], + "8657": [0.19444, 0.69444, 0, 0], + "8658": [-0.10889, 0.39111, 0, 0], + "8659": [0.19444, 0.69444, 0, 0], + "8660": [-0.10889, 0.39111, 0, 0], + "8661": [0.25, 0.75, 0, 0], + "8704": [0, 0.69444, 0, 0], + "8706": [0, 0.69444, 0.06389, 0], + "8707": [0, 0.69444, 0, 0], + "8709": [0.05556, 0.75, 0, 0], + "8711": [0, 0.68611, 0, 0], + "8712": [0.08556, 0.58556, 0, 0], + "8715": [0.08556, 0.58556, 0, 0], + "8722": [0.13333, 0.63333, 0, 0], + "8723": [0.13333, 0.63333, 0, 0], + "8725": [0.25, 0.75, 0, 0], + "8726": [0.25, 0.75, 0, 0], + "8727": [-0.02778, 0.47222, 0, 0], + "8728": [-0.02639, 0.47361, 0, 0], + "8729": [-0.02639, 0.47361, 0, 0], + "8730": [0.18, 0.82, 0, 0], + "8733": [0, 0.44444, 0, 0], + "8734": [0, 0.44444, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8739": [0.25, 0.75, 0, 0], + "8741": [0.25, 0.75, 0, 0], + "8743": [0, 0.55556, 0, 0], + "8744": [0, 0.55556, 0, 0], + "8745": [0, 0.55556, 0, 0], + "8746": [0, 0.55556, 0, 0], + "8747": [0.19444, 0.69444, 0.12778, 0], + "8764": [-0.10889, 0.39111, 0, 0], + "8768": [0.19444, 0.69444, 0, 0], + "8771": [0.00222, 0.50222, 0, 0], + "8776": [0.02444, 0.52444, 0, 0], + "8781": [0.00222, 0.50222, 0, 0], + "8801": [0.00222, 0.50222, 0, 0], + "8804": [0.19667, 0.69667, 0, 0], + "8805": [0.19667, 0.69667, 0, 0], + "8810": [0.08556, 0.58556, 0, 0], + "8811": [0.08556, 0.58556, 0, 0], + "8826": [0.08556, 0.58556, 0, 0], + "8827": [0.08556, 0.58556, 0, 0], + "8834": [0.08556, 0.58556, 0, 0], + "8835": [0.08556, 0.58556, 0, 0], + "8838": [0.19667, 0.69667, 0, 0], + "8839": [0.19667, 0.69667, 0, 0], + "8846": [0, 0.55556, 0, 0], + "8849": [0.19667, 0.69667, 0, 0], + "8850": [0.19667, 0.69667, 0, 0], + "8851": [0, 0.55556, 0, 0], + "8852": [0, 0.55556, 0, 0], + "8853": [0.13333, 0.63333, 0, 0], + "8854": [0.13333, 0.63333, 0, 0], + "8855": [0.13333, 0.63333, 0, 0], + "8856": [0.13333, 0.63333, 0, 0], + "8857": [0.13333, 0.63333, 0, 0], + "8866": [0, 0.69444, 0, 0], + "8867": [0, 0.69444, 0, 0], + "8868": [0, 0.69444, 0, 0], + "8869": [0, 0.69444, 0, 0], + "8900": [-0.02639, 0.47361, 0, 0], + "8901": [-0.02639, 0.47361, 0, 0], + "8902": [-0.02778, 0.47222, 0, 0], + "8968": [0.25, 0.75, 0, 0], + "8969": [0.25, 0.75, 0, 0], + "8970": [0.25, 0.75, 0, 0], + "8971": [0.25, 0.75, 0, 0], + "8994": [-0.13889, 0.36111, 0, 0], + "8995": [-0.13889, 0.36111, 0, 0], + "9651": [0.19444, 0.69444, 0, 0], + "9657": [-0.02778, 0.47222, 0, 0], + "9661": [0.19444, 0.69444, 0, 0], + "9667": [-0.02778, 0.47222, 0, 0], + "9711": [0.19444, 0.69444, 0, 0], + "9824": [0.12963, 0.69444, 0, 0], + "9825": [0.12963, 0.69444, 0, 0], + "9826": [0.12963, 0.69444, 0, 0], + "9827": [0.12963, 0.69444, 0, 0], + "9837": [0, 0.75, 0, 0], + "9838": [0.19444, 0.69444, 0, 0], + "9839": [0.19444, 0.69444, 0, 0], + "10216": [0.25, 0.75, 0, 0], + "10217": [0.25, 0.75, 0, 0], + "10815": [0, 0.68611, 0, 0], + "10927": [0.19667, 0.69667, 0, 0], + "10928": [0.19667, 0.69667, 0, 0], + }, + "Main-Italic": { + "33": [0, 0.69444, 0.12417, 0], + "34": [0, 0.69444, 0.06961, 0], + "35": [0.19444, 0.69444, 0.06616, 0], + "37": [0.05556, 0.75, 0.13639, 0], + "38": [0, 0.69444, 0.09694, 0], + "39": [0, 0.69444, 0.12417, 0], + "40": [0.25, 0.75, 0.16194, 0], + "41": [0.25, 0.75, 0.03694, 0], + "42": [0, 0.75, 0.14917, 0], + "43": [0.05667, 0.56167, 0.03694, 0], + "44": [0.19444, 0.10556, 0, 0], + "45": [0, 0.43056, 0.02826, 0], + "46": [0, 0.10556, 0, 0], + "47": [0.25, 0.75, 0.16194, 0], + "48": [0, 0.64444, 0.13556, 0], + "49": [0, 0.64444, 0.13556, 0], + "50": [0, 0.64444, 0.13556, 0], + "51": [0, 0.64444, 0.13556, 0], + "52": [0.19444, 0.64444, 0.13556, 0], + "53": [0, 0.64444, 0.13556, 0], + "54": [0, 0.64444, 0.13556, 0], + "55": [0.19444, 0.64444, 0.13556, 0], + "56": [0, 0.64444, 0.13556, 0], + "57": [0, 0.64444, 0.13556, 0], + "58": [0, 0.43056, 0.0582, 0], + "59": [0.19444, 0.43056, 0.0582, 0], + "61": [-0.13313, 0.36687, 0.06616, 0], + "63": [0, 0.69444, 0.1225, 0], + "64": [0, 0.69444, 0.09597, 0], + "65": [0, 0.68333, 0, 0], + "66": [0, 0.68333, 0.10257, 0], + "67": [0, 0.68333, 0.14528, 0], + "68": [0, 0.68333, 0.09403, 0], + "69": [0, 0.68333, 0.12028, 0], + "70": [0, 0.68333, 0.13305, 0], + "71": [0, 0.68333, 0.08722, 0], + "72": [0, 0.68333, 0.16389, 0], + "73": [0, 0.68333, 0.15806, 0], + "74": [0, 0.68333, 0.14028, 0], + "75": [0, 0.68333, 0.14528, 0], + "76": [0, 0.68333, 0, 0], + "77": [0, 0.68333, 0.16389, 0], + "78": [0, 0.68333, 0.16389, 0], + "79": [0, 0.68333, 0.09403, 0], + "80": [0, 0.68333, 0.10257, 0], + "81": [0.19444, 0.68333, 0.09403, 0], + "82": [0, 0.68333, 0.03868, 0], + "83": [0, 0.68333, 0.11972, 0], + "84": [0, 0.68333, 0.13305, 0], + "85": [0, 0.68333, 0.16389, 0], + "86": [0, 0.68333, 0.18361, 0], + "87": [0, 0.68333, 0.18361, 0], + "88": [0, 0.68333, 0.15806, 0], + "89": [0, 0.68333, 0.19383, 0], + "90": [0, 0.68333, 0.14528, 0], + "91": [0.25, 0.75, 0.1875, 0], + "93": [0.25, 0.75, 0.10528, 0], + "94": [0, 0.69444, 0.06646, 0], + "95": [0.31, 0.12056, 0.09208, 0], + "97": [0, 0.43056, 0.07671, 0], + "98": [0, 0.69444, 0.06312, 0], + "99": [0, 0.43056, 0.05653, 0], + "100": [0, 0.69444, 0.10333, 0], + "101": [0, 0.43056, 0.07514, 0], + "102": [0.19444, 0.69444, 0.21194, 0], + "103": [0.19444, 0.43056, 0.08847, 0], + "104": [0, 0.69444, 0.07671, 0], + "105": [0, 0.65536, 0.1019, 0], + "106": [0.19444, 0.65536, 0.14467, 0], + "107": [0, 0.69444, 0.10764, 0], + "108": [0, 0.69444, 0.10333, 0], + "109": [0, 0.43056, 0.07671, 0], + "110": [0, 0.43056, 0.07671, 0], + "111": [0, 0.43056, 0.06312, 0], + "112": [0.19444, 0.43056, 0.06312, 0], + "113": [0.19444, 0.43056, 0.08847, 0], + "114": [0, 0.43056, 0.10764, 0], + "115": [0, 0.43056, 0.08208, 0], + "116": [0, 0.61508, 0.09486, 0], + "117": [0, 0.43056, 0.07671, 0], + "118": [0, 0.43056, 0.10764, 0], + "119": [0, 0.43056, 0.10764, 0], + "120": [0, 0.43056, 0.12042, 0], + "121": [0.19444, 0.43056, 0.08847, 0], + "122": [0, 0.43056, 0.12292, 0], + "126": [0.35, 0.31786, 0.11585, 0], + "163": [0, 0.69444, 0, 0], + "305": [0, 0.43056, 0, 0.02778], + "567": [0.19444, 0.43056, 0, 0.08334], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0.09694, 0], + "770": [0, 0.69444, 0.06646, 0], + "771": [0, 0.66786, 0.11585, 0], + "772": [0, 0.56167, 0.10333, 0], + "774": [0, 0.69444, 0.10806, 0], + "775": [0, 0.66786, 0.11752, 0], + "776": [0, 0.66786, 0.10474, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0.1225, 0], + "780": [0, 0.62847, 0.08295, 0], + "915": [0, 0.68333, 0.13305, 0], + "916": [0, 0.68333, 0, 0], + "920": [0, 0.68333, 0.09403, 0], + "923": [0, 0.68333, 0, 0], + "926": [0, 0.68333, 0.15294, 0], + "928": [0, 0.68333, 0.16389, 0], + "931": [0, 0.68333, 0.12028, 0], + "933": [0, 0.68333, 0.11111, 0], + "934": [0, 0.68333, 0.05986, 0], + "936": [0, 0.68333, 0.11111, 0], + "937": [0, 0.68333, 0.10257, 0], + "8211": [0, 0.43056, 0.09208, 0], + "8212": [0, 0.43056, 0.09208, 0], + "8216": [0, 0.69444, 0.12417, 0], + "8217": [0, 0.69444, 0.12417, 0], + "8220": [0, 0.69444, 0.1685, 0], + "8221": [0, 0.69444, 0.06961, 0], + "8463": [0, 0.68889, 0, 0], + }, + "Main-Regular": { + "32": [0, 0, 0, 0], + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.08333, 0.58333, 0, 0], + "44": [0.19444, 0.10556, 0, 0], + "45": [0, 0.43056, 0, 0], + "46": [0, 0.10556, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.64444, 0, 0], + "49": [0, 0.64444, 0, 0], + "50": [0, 0.64444, 0, 0], + "51": [0, 0.64444, 0, 0], + "52": [0, 0.64444, 0, 0], + "53": [0, 0.64444, 0, 0], + "54": [0, 0.64444, 0, 0], + "55": [0, 0.64444, 0, 0], + "56": [0, 0.64444, 0, 0], + "57": [0, 0.64444, 0, 0], + "58": [0, 0.43056, 0, 0], + "59": [0.19444, 0.43056, 0, 0], + "60": [0.0391, 0.5391, 0, 0], + "61": [-0.13313, 0.36687, 0, 0], + "62": [0.0391, 0.5391, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.68333, 0, 0], + "66": [0, 0.68333, 0, 0], + "67": [0, 0.68333, 0, 0], + "68": [0, 0.68333, 0, 0], + "69": [0, 0.68333, 0, 0], + "70": [0, 0.68333, 0, 0], + "71": [0, 0.68333, 0, 0], + "72": [0, 0.68333, 0, 0], + "73": [0, 0.68333, 0, 0], + "74": [0, 0.68333, 0, 0], + "75": [0, 0.68333, 0, 0], + "76": [0, 0.68333, 0, 0], + "77": [0, 0.68333, 0, 0], + "78": [0, 0.68333, 0, 0], + "79": [0, 0.68333, 0, 0], + "80": [0, 0.68333, 0, 0], + "81": [0.19444, 0.68333, 0, 0], + "82": [0, 0.68333, 0, 0], + "83": [0, 0.68333, 0, 0], + "84": [0, 0.68333, 0, 0], + "85": [0, 0.68333, 0, 0], + "86": [0, 0.68333, 0.01389, 0], + "87": [0, 0.68333, 0.01389, 0], + "88": [0, 0.68333, 0, 0], + "89": [0, 0.68333, 0.025, 0], + "90": [0, 0.68333, 0, 0], + "91": [0.25, 0.75, 0, 0], + "92": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.31, 0.12056, 0.02778, 0], + "96": [0, 0.69444, 0, 0], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.43056, 0, 0], + "102": [0, 0.69444, 0.07778, 0], + "103": [0.19444, 0.43056, 0.01389, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.66786, 0, 0], + "106": [0.19444, 0.66786, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0], + "112": [0.19444, 0.43056, 0, 0], + "113": [0.19444, 0.43056, 0, 0], + "114": [0, 0.43056, 0, 0], + "115": [0, 0.43056, 0, 0], + "116": [0, 0.61508, 0, 0], + "117": [0, 0.43056, 0, 0], + "118": [0, 0.43056, 0.01389, 0], + "119": [0, 0.43056, 0.01389, 0], + "120": [0, 0.43056, 0, 0], + "121": [0.19444, 0.43056, 0.01389, 0], + "122": [0, 0.43056, 0, 0], + "123": [0.25, 0.75, 0, 0], + "124": [0.25, 0.75, 0, 0], + "125": [0.25, 0.75, 0, 0], + "126": [0.35, 0.31786, 0, 0], + "160": [0, 0, 0, 0], + "168": [0, 0.66786, 0, 0], + "172": [0, 0.43056, 0, 0], + "175": [0, 0.56778, 0, 0], + "176": [0, 0.69444, 0, 0], + "177": [0.08333, 0.58333, 0, 0], + "180": [0, 0.69444, 0, 0], + "215": [0.08333, 0.58333, 0, 0], + "247": [0.08333, 0.58333, 0, 0], + "305": [0, 0.43056, 0, 0], + "567": [0.19444, 0.43056, 0, 0], + "710": [0, 0.69444, 0, 0], + "711": [0, 0.62847, 0, 0], + "713": [0, 0.56778, 0, 0], + "714": [0, 0.69444, 0, 0], + "715": [0, 0.69444, 0, 0], + "728": [0, 0.69444, 0, 0], + "729": [0, 0.66786, 0, 0], + "730": [0, 0.69444, 0, 0], + "732": [0, 0.66786, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.66786, 0, 0], + "772": [0, 0.56778, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.66786, 0, 0], + "776": [0, 0.66786, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.62847, 0, 0], + "824": [0.19444, 0.69444, 0, 0], + "915": [0, 0.68333, 0, 0], + "916": [0, 0.68333, 0, 0], + "920": [0, 0.68333, 0, 0], + "923": [0, 0.68333, 0, 0], + "926": [0, 0.68333, 0, 0], + "928": [0, 0.68333, 0, 0], + "931": [0, 0.68333, 0, 0], + "933": [0, 0.68333, 0, 0], + "934": [0, 0.68333, 0, 0], + "936": [0, 0.68333, 0, 0], + "937": [0, 0.68333, 0, 0], + "8211": [0, 0.43056, 0.02778, 0], + "8212": [0, 0.43056, 0.02778, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + "8224": [0.19444, 0.69444, 0, 0], + "8225": [0.19444, 0.69444, 0, 0], + "8230": [0, 0.12, 0, 0], + "8242": [0, 0.55556, 0, 0], + "8407": [0, 0.71444, 0.15382, 0], + "8463": [0, 0.68889, 0, 0], + "8465": [0, 0.69444, 0, 0], + "8467": [0, 0.69444, 0, 0.11111], + "8472": [0.19444, 0.43056, 0, 0.11111], + "8476": [0, 0.69444, 0, 0], + "8501": [0, 0.69444, 0, 0], + "8592": [-0.13313, 0.36687, 0, 0], + "8593": [0.19444, 0.69444, 0, 0], + "8594": [-0.13313, 0.36687, 0, 0], + "8595": [0.19444, 0.69444, 0, 0], + "8596": [-0.13313, 0.36687, 0, 0], + "8597": [0.25, 0.75, 0, 0], + "8598": [0.19444, 0.69444, 0, 0], + "8599": [0.19444, 0.69444, 0, 0], + "8600": [0.19444, 0.69444, 0, 0], + "8601": [0.19444, 0.69444, 0, 0], + "8614": [0.011, 0.511, 0, 0], + "8617": [0.011, 0.511, 0, 0], + "8618": [0.011, 0.511, 0, 0], + "8636": [-0.13313, 0.36687, 0, 0], + "8637": [-0.13313, 0.36687, 0, 0], + "8640": [-0.13313, 0.36687, 0, 0], + "8641": [-0.13313, 0.36687, 0, 0], + "8652": [0.011, 0.671, 0, 0], + "8656": [-0.13313, 0.36687, 0, 0], + "8657": [0.19444, 0.69444, 0, 0], + "8658": [-0.13313, 0.36687, 0, 0], + "8659": [0.19444, 0.69444, 0, 0], + "8660": [-0.13313, 0.36687, 0, 0], + "8661": [0.25, 0.75, 0, 0], + "8704": [0, 0.69444, 0, 0], + "8706": [0, 0.69444, 0.05556, 0.08334], + "8707": [0, 0.69444, 0, 0], + "8709": [0.05556, 0.75, 0, 0], + "8711": [0, 0.68333, 0, 0], + "8712": [0.0391, 0.5391, 0, 0], + "8715": [0.0391, 0.5391, 0, 0], + "8722": [0.08333, 0.58333, 0, 0], + "8723": [0.08333, 0.58333, 0, 0], + "8725": [0.25, 0.75, 0, 0], + "8726": [0.25, 0.75, 0, 0], + "8727": [-0.03472, 0.46528, 0, 0], + "8728": [-0.05555, 0.44445, 0, 0], + "8729": [-0.05555, 0.44445, 0, 0], + "8730": [0.2, 0.8, 0, 0], + "8733": [0, 0.43056, 0, 0], + "8734": [0, 0.43056, 0, 0], + "8736": [0, 0.69224, 0, 0], + "8739": [0.25, 0.75, 0, 0], + "8741": [0.25, 0.75, 0, 0], + "8743": [0, 0.55556, 0, 0], + "8744": [0, 0.55556, 0, 0], + "8745": [0, 0.55556, 0, 0], + "8746": [0, 0.55556, 0, 0], + "8747": [0.19444, 0.69444, 0.11111, 0], + "8764": [-0.13313, 0.36687, 0, 0], + "8768": [0.19444, 0.69444, 0, 0], + "8771": [-0.03625, 0.46375, 0, 0], + "8773": [-0.022, 0.589, 0, 0], + "8776": [-0.01688, 0.48312, 0, 0], + "8781": [-0.03625, 0.46375, 0, 0], + "8784": [-0.133, 0.67, 0, 0], + "8800": [0.215, 0.716, 0, 0], + "8801": [-0.03625, 0.46375, 0, 0], + "8804": [0.13597, 0.63597, 0, 0], + "8805": [0.13597, 0.63597, 0, 0], + "8810": [0.0391, 0.5391, 0, 0], + "8811": [0.0391, 0.5391, 0, 0], + "8826": [0.0391, 0.5391, 0, 0], + "8827": [0.0391, 0.5391, 0, 0], + "8834": [0.0391, 0.5391, 0, 0], + "8835": [0.0391, 0.5391, 0, 0], + "8838": [0.13597, 0.63597, 0, 0], + "8839": [0.13597, 0.63597, 0, 0], + "8846": [0, 0.55556, 0, 0], + "8849": [0.13597, 0.63597, 0, 0], + "8850": [0.13597, 0.63597, 0, 0], + "8851": [0, 0.55556, 0, 0], + "8852": [0, 0.55556, 0, 0], + "8853": [0.08333, 0.58333, 0, 0], + "8854": [0.08333, 0.58333, 0, 0], + "8855": [0.08333, 0.58333, 0, 0], + "8856": [0.08333, 0.58333, 0, 0], + "8857": [0.08333, 0.58333, 0, 0], + "8866": [0, 0.69444, 0, 0], + "8867": [0, 0.69444, 0, 0], + "8868": [0, 0.69444, 0, 0], + "8869": [0, 0.69444, 0, 0], + "8872": [0.249, 0.75, 0, 0], + "8900": [-0.05555, 0.44445, 0, 0], + "8901": [-0.05555, 0.44445, 0, 0], + "8902": [-0.03472, 0.46528, 0, 0], + "8904": [0.005, 0.505, 0, 0], + "8942": [0.03, 0.9, 0, 0], + "8943": [-0.19, 0.31, 0, 0], + "8945": [-0.1, 0.82, 0, 0], + "8968": [0.25, 0.75, 0, 0], + "8969": [0.25, 0.75, 0, 0], + "8970": [0.25, 0.75, 0, 0], + "8971": [0.25, 0.75, 0, 0], + "8994": [-0.14236, 0.35764, 0, 0], + "8995": [-0.14236, 0.35764, 0, 0], + "9136": [0.244, 0.744, 0, 0], + "9137": [0.244, 0.744, 0, 0], + "9651": [0.19444, 0.69444, 0, 0], + "9657": [-0.03472, 0.46528, 0, 0], + "9661": [0.19444, 0.69444, 0, 0], + "9667": [-0.03472, 0.46528, 0, 0], + "9711": [0.19444, 0.69444, 0, 0], + "9824": [0.12963, 0.69444, 0, 0], + "9825": [0.12963, 0.69444, 0, 0], + "9826": [0.12963, 0.69444, 0, 0], + "9827": [0.12963, 0.69444, 0, 0], + "9837": [0, 0.75, 0, 0], + "9838": [0.19444, 0.69444, 0, 0], + "9839": [0.19444, 0.69444, 0, 0], + "10216": [0.25, 0.75, 0, 0], + "10217": [0.25, 0.75, 0, 0], + "10222": [0.244, 0.744, 0, 0], + "10223": [0.244, 0.744, 0, 0], + "10229": [0.011, 0.511, 0, 0], + "10230": [0.011, 0.511, 0, 0], + "10231": [0.011, 0.511, 0, 0], + "10232": [0.024, 0.525, 0, 0], + "10233": [0.024, 0.525, 0, 0], + "10234": [0.024, 0.525, 0, 0], + "10236": [0.011, 0.511, 0, 0], + "10815": [0, 0.68333, 0, 0], + "10927": [0.13597, 0.63597, 0, 0], + "10928": [0.13597, 0.63597, 0, 0], + }, + "Math-BoldItalic": { + "47": [0.19444, 0.69444, 0, 0], + "65": [0, 0.68611, 0, 0], + "66": [0, 0.68611, 0.04835, 0], + "67": [0, 0.68611, 0.06979, 0], + "68": [0, 0.68611, 0.03194, 0], + "69": [0, 0.68611, 0.05451, 0], + "70": [0, 0.68611, 0.15972, 0], + "71": [0, 0.68611, 0, 0], + "72": [0, 0.68611, 0.08229, 0], + "73": [0, 0.68611, 0.07778, 0], + "74": [0, 0.68611, 0.10069, 0], + "75": [0, 0.68611, 0.06979, 0], + "76": [0, 0.68611, 0, 0], + "77": [0, 0.68611, 0.11424, 0], + "78": [0, 0.68611, 0.11424, 0], + "79": [0, 0.68611, 0.03194, 0], + "80": [0, 0.68611, 0.15972, 0], + "81": [0.19444, 0.68611, 0, 0], + "82": [0, 0.68611, 0.00421, 0], + "83": [0, 0.68611, 0.05382, 0], + "84": [0, 0.68611, 0.15972, 0], + "85": [0, 0.68611, 0.11424, 0], + "86": [0, 0.68611, 0.25555, 0], + "87": [0, 0.68611, 0.15972, 0], + "88": [0, 0.68611, 0.07778, 0], + "89": [0, 0.68611, 0.25555, 0], + "90": [0, 0.68611, 0.06979, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0.19444, 0.69444, 0.11042, 0], + "103": [0.19444, 0.44444, 0.03704, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.69326, 0, 0], + "106": [0.19444, 0.69326, 0.0622, 0], + "107": [0, 0.69444, 0.01852, 0], + "108": [0, 0.69444, 0.0088, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0.03704, 0], + "114": [0, 0.44444, 0.03194, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.63492, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.03704, 0], + "119": [0, 0.44444, 0.02778, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.03704, 0], + "122": [0, 0.44444, 0.04213, 0], + "915": [0, 0.68611, 0.15972, 0], + "916": [0, 0.68611, 0, 0], + "920": [0, 0.68611, 0.03194, 0], + "923": [0, 0.68611, 0, 0], + "926": [0, 0.68611, 0.07458, 0], + "928": [0, 0.68611, 0.08229, 0], + "931": [0, 0.68611, 0.05451, 0], + "933": [0, 0.68611, 0.15972, 0], + "934": [0, 0.68611, 0, 0], + "936": [0, 0.68611, 0.11653, 0], + "937": [0, 0.68611, 0.04835, 0], + "945": [0, 0.44444, 0, 0], + "946": [0.19444, 0.69444, 0.03403, 0], + "947": [0.19444, 0.44444, 0.06389, 0], + "948": [0, 0.69444, 0.03819, 0], + "949": [0, 0.44444, 0, 0], + "950": [0.19444, 0.69444, 0.06215, 0], + "951": [0.19444, 0.44444, 0.03704, 0], + "952": [0, 0.69444, 0.03194, 0], + "953": [0, 0.44444, 0, 0], + "954": [0, 0.44444, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.44444, 0, 0], + "957": [0, 0.44444, 0.06898, 0], + "958": [0.19444, 0.69444, 0.03021, 0], + "959": [0, 0.44444, 0, 0], + "960": [0, 0.44444, 0.03704, 0], + "961": [0.19444, 0.44444, 0, 0], + "962": [0.09722, 0.44444, 0.07917, 0], + "963": [0, 0.44444, 0.03704, 0], + "964": [0, 0.44444, 0.13472, 0], + "965": [0, 0.44444, 0.03704, 0], + "966": [0.19444, 0.44444, 0, 0], + "967": [0.19444, 0.44444, 0, 0], + "968": [0.19444, 0.69444, 0.03704, 0], + "969": [0, 0.44444, 0.03704, 0], + "977": [0, 0.69444, 0, 0], + "981": [0.19444, 0.69444, 0, 0], + "982": [0, 0.44444, 0.03194, 0], + "1009": [0.19444, 0.44444, 0, 0], + "1013": [0, 0.44444, 0, 0], + }, + "Math-Italic": { + "47": [0.19444, 0.69444, 0, 0], + "65": [0, 0.68333, 0, 0.13889], + "66": [0, 0.68333, 0.05017, 0.08334], + "67": [0, 0.68333, 0.07153, 0.08334], + "68": [0, 0.68333, 0.02778, 0.05556], + "69": [0, 0.68333, 0.05764, 0.08334], + "70": [0, 0.68333, 0.13889, 0.08334], + "71": [0, 0.68333, 0, 0.08334], + "72": [0, 0.68333, 0.08125, 0.05556], + "73": [0, 0.68333, 0.07847, 0.11111], + "74": [0, 0.68333, 0.09618, 0.16667], + "75": [0, 0.68333, 0.07153, 0.05556], + "76": [0, 0.68333, 0, 0.02778], + "77": [0, 0.68333, 0.10903, 0.08334], + "78": [0, 0.68333, 0.10903, 0.08334], + "79": [0, 0.68333, 0.02778, 0.08334], + "80": [0, 0.68333, 0.13889, 0.08334], + "81": [0.19444, 0.68333, 0, 0.08334], + "82": [0, 0.68333, 0.00773, 0.08334], + "83": [0, 0.68333, 0.05764, 0.08334], + "84": [0, 0.68333, 0.13889, 0.08334], + "85": [0, 0.68333, 0.10903, 0.02778], + "86": [0, 0.68333, 0.22222, 0], + "87": [0, 0.68333, 0.13889, 0], + "88": [0, 0.68333, 0.07847, 0.08334], + "89": [0, 0.68333, 0.22222, 0], + "90": [0, 0.68333, 0.07153, 0.08334], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0.05556], + "100": [0, 0.69444, 0, 0.16667], + "101": [0, 0.43056, 0, 0.05556], + "102": [0.19444, 0.69444, 0.10764, 0.16667], + "103": [0.19444, 0.43056, 0.03588, 0.02778], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.65952, 0, 0], + "106": [0.19444, 0.65952, 0.05724, 0], + "107": [0, 0.69444, 0.03148, 0], + "108": [0, 0.69444, 0.01968, 0.08334], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0.05556], + "112": [0.19444, 0.43056, 0, 0.08334], + "113": [0.19444, 0.43056, 0.03588, 0.08334], + "114": [0, 0.43056, 0.02778, 0.05556], + "115": [0, 0.43056, 0, 0.05556], + "116": [0, 0.61508, 0, 0.08334], + "117": [0, 0.43056, 0, 0.02778], + "118": [0, 0.43056, 0.03588, 0.02778], + "119": [0, 0.43056, 0.02691, 0.08334], + "120": [0, 0.43056, 0, 0.02778], + "121": [0.19444, 0.43056, 0.03588, 0.05556], + "122": [0, 0.43056, 0.04398, 0.05556], + "915": [0, 0.68333, 0.13889, 0.08334], + "916": [0, 0.68333, 0, 0.16667], + "920": [0, 0.68333, 0.02778, 0.08334], + "923": [0, 0.68333, 0, 0.16667], + "926": [0, 0.68333, 0.07569, 0.08334], + "928": [0, 0.68333, 0.08125, 0.05556], + "931": [0, 0.68333, 0.05764, 0.08334], + "933": [0, 0.68333, 0.13889, 0.05556], + "934": [0, 0.68333, 0, 0.08334], + "936": [0, 0.68333, 0.11, 0.05556], + "937": [0, 0.68333, 0.05017, 0.08334], + "945": [0, 0.43056, 0.0037, 0.02778], + "946": [0.19444, 0.69444, 0.05278, 0.08334], + "947": [0.19444, 0.43056, 0.05556, 0], + "948": [0, 0.69444, 0.03785, 0.05556], + "949": [0, 0.43056, 0, 0.08334], + "950": [0.19444, 0.69444, 0.07378, 0.08334], + "951": [0.19444, 0.43056, 0.03588, 0.05556], + "952": [0, 0.69444, 0.02778, 0.08334], + "953": [0, 0.43056, 0, 0.05556], + "954": [0, 0.43056, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.43056, 0, 0.02778], + "957": [0, 0.43056, 0.06366, 0.02778], + "958": [0.19444, 0.69444, 0.04601, 0.11111], + "959": [0, 0.43056, 0, 0.05556], + "960": [0, 0.43056, 0.03588, 0], + "961": [0.19444, 0.43056, 0, 0.08334], + "962": [0.09722, 0.43056, 0.07986, 0.08334], + "963": [0, 0.43056, 0.03588, 0], + "964": [0, 0.43056, 0.1132, 0.02778], + "965": [0, 0.43056, 0.03588, 0.02778], + "966": [0.19444, 0.43056, 0, 0.08334], + "967": [0.19444, 0.43056, 0, 0.05556], + "968": [0.19444, 0.69444, 0.03588, 0.11111], + "969": [0, 0.43056, 0.03588, 0], + "977": [0, 0.69444, 0, 0.08334], + "981": [0.19444, 0.69444, 0, 0.08334], + "982": [0, 0.43056, 0.02778, 0], + "1009": [0.19444, 0.43056, 0, 0.08334], + "1013": [0, 0.43056, 0, 0.05556], + }, + "Math-Regular": { + "65": [0, 0.68333, 0, 0.13889], + "66": [0, 0.68333, 0.05017, 0.08334], + "67": [0, 0.68333, 0.07153, 0.08334], + "68": [0, 0.68333, 0.02778, 0.05556], + "69": [0, 0.68333, 0.05764, 0.08334], + "70": [0, 0.68333, 0.13889, 0.08334], + "71": [0, 0.68333, 0, 0.08334], + "72": [0, 0.68333, 0.08125, 0.05556], + "73": [0, 0.68333, 0.07847, 0.11111], + "74": [0, 0.68333, 0.09618, 0.16667], + "75": [0, 0.68333, 0.07153, 0.05556], + "76": [0, 0.68333, 0, 0.02778], + "77": [0, 0.68333, 0.10903, 0.08334], + "78": [0, 0.68333, 0.10903, 0.08334], + "79": [0, 0.68333, 0.02778, 0.08334], + "80": [0, 0.68333, 0.13889, 0.08334], + "81": [0.19444, 0.68333, 0, 0.08334], + "82": [0, 0.68333, 0.00773, 0.08334], + "83": [0, 0.68333, 0.05764, 0.08334], + "84": [0, 0.68333, 0.13889, 0.08334], + "85": [0, 0.68333, 0.10903, 0.02778], + "86": [0, 0.68333, 0.22222, 0], + "87": [0, 0.68333, 0.13889, 0], + "88": [0, 0.68333, 0.07847, 0.08334], + "89": [0, 0.68333, 0.22222, 0], + "90": [0, 0.68333, 0.07153, 0.08334], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.43056, 0, 0.05556], + "100": [0, 0.69444, 0, 0.16667], + "101": [0, 0.43056, 0, 0.05556], + "102": [0.19444, 0.69444, 0.10764, 0.16667], + "103": [0.19444, 0.43056, 0.03588, 0.02778], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.65952, 0, 0], + "106": [0.19444, 0.65952, 0.05724, 0], + "107": [0, 0.69444, 0.03148, 0], + "108": [0, 0.69444, 0.01968, 0.08334], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0.05556], + "112": [0.19444, 0.43056, 0, 0.08334], + "113": [0.19444, 0.43056, 0.03588, 0.08334], + "114": [0, 0.43056, 0.02778, 0.05556], + "115": [0, 0.43056, 0, 0.05556], + "116": [0, 0.61508, 0, 0.08334], + "117": [0, 0.43056, 0, 0.02778], + "118": [0, 0.43056, 0.03588, 0.02778], + "119": [0, 0.43056, 0.02691, 0.08334], + "120": [0, 0.43056, 0, 0.02778], + "121": [0.19444, 0.43056, 0.03588, 0.05556], + "122": [0, 0.43056, 0.04398, 0.05556], + "915": [0, 0.68333, 0.13889, 0.08334], + "916": [0, 0.68333, 0, 0.16667], + "920": [0, 0.68333, 0.02778, 0.08334], + "923": [0, 0.68333, 0, 0.16667], + "926": [0, 0.68333, 0.07569, 0.08334], + "928": [0, 0.68333, 0.08125, 0.05556], + "931": [0, 0.68333, 0.05764, 0.08334], + "933": [0, 0.68333, 0.13889, 0.05556], + "934": [0, 0.68333, 0, 0.08334], + "936": [0, 0.68333, 0.11, 0.05556], + "937": [0, 0.68333, 0.05017, 0.08334], + "945": [0, 0.43056, 0.0037, 0.02778], + "946": [0.19444, 0.69444, 0.05278, 0.08334], + "947": [0.19444, 0.43056, 0.05556, 0], + "948": [0, 0.69444, 0.03785, 0.05556], + "949": [0, 0.43056, 0, 0.08334], + "950": [0.19444, 0.69444, 0.07378, 0.08334], + "951": [0.19444, 0.43056, 0.03588, 0.05556], + "952": [0, 0.69444, 0.02778, 0.08334], + "953": [0, 0.43056, 0, 0.05556], + "954": [0, 0.43056, 0, 0], + "955": [0, 0.69444, 0, 0], + "956": [0.19444, 0.43056, 0, 0.02778], + "957": [0, 0.43056, 0.06366, 0.02778], + "958": [0.19444, 0.69444, 0.04601, 0.11111], + "959": [0, 0.43056, 0, 0.05556], + "960": [0, 0.43056, 0.03588, 0], + "961": [0.19444, 0.43056, 0, 0.08334], + "962": [0.09722, 0.43056, 0.07986, 0.08334], + "963": [0, 0.43056, 0.03588, 0], + "964": [0, 0.43056, 0.1132, 0.02778], + "965": [0, 0.43056, 0.03588, 0.02778], + "966": [0.19444, 0.43056, 0, 0.08334], + "967": [0.19444, 0.43056, 0, 0.05556], + "968": [0.19444, 0.69444, 0.03588, 0.11111], + "969": [0, 0.43056, 0.03588, 0], + "977": [0, 0.69444, 0, 0.08334], + "981": [0.19444, 0.69444, 0, 0.08334], + "982": [0, 0.43056, 0.02778, 0], + "1009": [0.19444, 0.43056, 0, 0.08334], + "1013": [0, 0.43056, 0, 0.05556], + }, + "SansSerif-Regular": { + "33": [0, 0.69444, 0, 0], + "34": [0, 0.69444, 0, 0], + "35": [0.19444, 0.69444, 0, 0], + "36": [0.05556, 0.75, 0, 0], + "37": [0.05556, 0.75, 0, 0], + "38": [0, 0.69444, 0, 0], + "39": [0, 0.69444, 0, 0], + "40": [0.25, 0.75, 0, 0], + "41": [0.25, 0.75, 0, 0], + "42": [0, 0.75, 0, 0], + "43": [0.08333, 0.58333, 0, 0], + "44": [0.125, 0.08333, 0, 0], + "45": [0, 0.44444, 0, 0], + "46": [0, 0.08333, 0, 0], + "47": [0.25, 0.75, 0, 0], + "48": [0, 0.65556, 0, 0], + "49": [0, 0.65556, 0, 0], + "50": [0, 0.65556, 0, 0], + "51": [0, 0.65556, 0, 0], + "52": [0, 0.65556, 0, 0], + "53": [0, 0.65556, 0, 0], + "54": [0, 0.65556, 0, 0], + "55": [0, 0.65556, 0, 0], + "56": [0, 0.65556, 0, 0], + "57": [0, 0.65556, 0, 0], + "58": [0, 0.44444, 0, 0], + "59": [0.125, 0.44444, 0, 0], + "61": [-0.13, 0.37, 0, 0], + "63": [0, 0.69444, 0, 0], + "64": [0, 0.69444, 0, 0], + "65": [0, 0.69444, 0, 0], + "66": [0, 0.69444, 0, 0], + "67": [0, 0.69444, 0, 0], + "68": [0, 0.69444, 0, 0], + "69": [0, 0.69444, 0, 0], + "70": [0, 0.69444, 0, 0], + "71": [0, 0.69444, 0, 0], + "72": [0, 0.69444, 0, 0], + "73": [0, 0.69444, 0, 0], + "74": [0, 0.69444, 0, 0], + "75": [0, 0.69444, 0, 0], + "76": [0, 0.69444, 0, 0], + "77": [0, 0.69444, 0, 0], + "78": [0, 0.69444, 0, 0], + "79": [0, 0.69444, 0, 0], + "80": [0, 0.69444, 0, 0], + "81": [0.125, 0.69444, 0, 0], + "82": [0, 0.69444, 0, 0], + "83": [0, 0.69444, 0, 0], + "84": [0, 0.69444, 0, 0], + "85": [0, 0.69444, 0, 0], + "86": [0, 0.69444, 0.01389, 0], + "87": [0, 0.69444, 0.01389, 0], + "88": [0, 0.69444, 0, 0], + "89": [0, 0.69444, 0.025, 0], + "90": [0, 0.69444, 0, 0], + "91": [0.25, 0.75, 0, 0], + "93": [0.25, 0.75, 0, 0], + "94": [0, 0.69444, 0, 0], + "95": [0.35, 0.09444, 0.02778, 0], + "97": [0, 0.44444, 0, 0], + "98": [0, 0.69444, 0, 0], + "99": [0, 0.44444, 0, 0], + "100": [0, 0.69444, 0, 0], + "101": [0, 0.44444, 0, 0], + "102": [0, 0.69444, 0.06944, 0], + "103": [0.19444, 0.44444, 0.01389, 0], + "104": [0, 0.69444, 0, 0], + "105": [0, 0.67937, 0, 0], + "106": [0.19444, 0.67937, 0, 0], + "107": [0, 0.69444, 0, 0], + "108": [0, 0.69444, 0, 0], + "109": [0, 0.44444, 0, 0], + "110": [0, 0.44444, 0, 0], + "111": [0, 0.44444, 0, 0], + "112": [0.19444, 0.44444, 0, 0], + "113": [0.19444, 0.44444, 0, 0], + "114": [0, 0.44444, 0.01389, 0], + "115": [0, 0.44444, 0, 0], + "116": [0, 0.57143, 0, 0], + "117": [0, 0.44444, 0, 0], + "118": [0, 0.44444, 0.01389, 0], + "119": [0, 0.44444, 0.01389, 0], + "120": [0, 0.44444, 0, 0], + "121": [0.19444, 0.44444, 0.01389, 0], + "122": [0, 0.44444, 0, 0], + "126": [0.35, 0.32659, 0, 0], + "305": [0, 0.44444, 0, 0], + "567": [0.19444, 0.44444, 0, 0], + "768": [0, 0.69444, 0, 0], + "769": [0, 0.69444, 0, 0], + "770": [0, 0.69444, 0, 0], + "771": [0, 0.67659, 0, 0], + "772": [0, 0.60889, 0, 0], + "774": [0, 0.69444, 0, 0], + "775": [0, 0.67937, 0, 0], + "776": [0, 0.67937, 0, 0], + "778": [0, 0.69444, 0, 0], + "779": [0, 0.69444, 0, 0], + "780": [0, 0.63194, 0, 0], + "915": [0, 0.69444, 0, 0], + "916": [0, 0.69444, 0, 0], + "920": [0, 0.69444, 0, 0], + "923": [0, 0.69444, 0, 0], + "926": [0, 0.69444, 0, 0], + "928": [0, 0.69444, 0, 0], + "931": [0, 0.69444, 0, 0], + "933": [0, 0.69444, 0, 0], + "934": [0, 0.69444, 0, 0], + "936": [0, 0.69444, 0, 0], + "937": [0, 0.69444, 0, 0], + "8211": [0, 0.44444, 0.02778, 0], + "8212": [0, 0.44444, 0.02778, 0], + "8216": [0, 0.69444, 0, 0], + "8217": [0, 0.69444, 0, 0], + "8220": [0, 0.69444, 0, 0], + "8221": [0, 0.69444, 0, 0], + }, + "Script-Regular": { + "65": [0, 0.7, 0.22925, 0], + "66": [0, 0.7, 0.04087, 0], + "67": [0, 0.7, 0.1689, 0], + "68": [0, 0.7, 0.09371, 0], + "69": [0, 0.7, 0.18583, 0], + "70": [0, 0.7, 0.13634, 0], + "71": [0, 0.7, 0.17322, 0], + "72": [0, 0.7, 0.29694, 0], + "73": [0, 0.7, 0.19189, 0], + "74": [0.27778, 0.7, 0.19189, 0], + "75": [0, 0.7, 0.31259, 0], + "76": [0, 0.7, 0.19189, 0], + "77": [0, 0.7, 0.15981, 0], + "78": [0, 0.7, 0.3525, 0], + "79": [0, 0.7, 0.08078, 0], + "80": [0, 0.7, 0.08078, 0], + "81": [0, 0.7, 0.03305, 0], + "82": [0, 0.7, 0.06259, 0], + "83": [0, 0.7, 0.19189, 0], + "84": [0, 0.7, 0.29087, 0], + "85": [0, 0.7, 0.25815, 0], + "86": [0, 0.7, 0.27523, 0], + "87": [0, 0.7, 0.27523, 0], + "88": [0, 0.7, 0.26006, 0], + "89": [0, 0.7, 0.2939, 0], + "90": [0, 0.7, 0.24037, 0], + }, + "Size1-Regular": { + "40": [0.35001, 0.85, 0, 0], + "41": [0.35001, 0.85, 0, 0], + "47": [0.35001, 0.85, 0, 0], + "91": [0.35001, 0.85, 0, 0], + "92": [0.35001, 0.85, 0, 0], + "93": [0.35001, 0.85, 0, 0], + "123": [0.35001, 0.85, 0, 0], + "125": [0.35001, 0.85, 0, 0], + "710": [0, 0.72222, 0, 0], + "732": [0, 0.72222, 0, 0], + "770": [0, 0.72222, 0, 0], + "771": [0, 0.72222, 0, 0], + "8214": [-0.00099, 0.601, 0, 0], + "8593": [1e-05, 0.6, 0, 0], + "8595": [1e-05, 0.6, 0, 0], + "8657": [1e-05, 0.6, 0, 0], + "8659": [1e-05, 0.6, 0, 0], + "8719": [0.25001, 0.75, 0, 0], + "8720": [0.25001, 0.75, 0, 0], + "8721": [0.25001, 0.75, 0, 0], + "8730": [0.35001, 0.85, 0, 0], + "8739": [-0.00599, 0.606, 0, 0], + "8741": [-0.00599, 0.606, 0, 0], + "8747": [0.30612, 0.805, 0.19445, 0], + "8748": [0.306, 0.805, 0.19445, 0], + "8749": [0.306, 0.805, 0.19445, 0], + "8750": [0.30612, 0.805, 0.19445, 0], + "8896": [0.25001, 0.75, 0, 0], + "8897": [0.25001, 0.75, 0, 0], + "8898": [0.25001, 0.75, 0, 0], + "8899": [0.25001, 0.75, 0, 0], + "8968": [0.35001, 0.85, 0, 0], + "8969": [0.35001, 0.85, 0, 0], + "8970": [0.35001, 0.85, 0, 0], + "8971": [0.35001, 0.85, 0, 0], + "9168": [-0.00099, 0.601, 0, 0], + "10216": [0.35001, 0.85, 0, 0], + "10217": [0.35001, 0.85, 0, 0], + "10752": [0.25001, 0.75, 0, 0], + "10753": [0.25001, 0.75, 0, 0], + "10754": [0.25001, 0.75, 0, 0], + "10756": [0.25001, 0.75, 0, 0], + "10758": [0.25001, 0.75, 0, 0], + }, + "Size2-Regular": { + "40": [0.65002, 1.15, 0, 0], + "41": [0.65002, 1.15, 0, 0], + "47": [0.65002, 1.15, 0, 0], + "91": [0.65002, 1.15, 0, 0], + "92": [0.65002, 1.15, 0, 0], + "93": [0.65002, 1.15, 0, 0], + "123": [0.65002, 1.15, 0, 0], + "125": [0.65002, 1.15, 0, 0], + "710": [0, 0.75, 0, 0], + "732": [0, 0.75, 0, 0], + "770": [0, 0.75, 0, 0], + "771": [0, 0.75, 0, 0], + "8719": [0.55001, 1.05, 0, 0], + "8720": [0.55001, 1.05, 0, 0], + "8721": [0.55001, 1.05, 0, 0], + "8730": [0.65002, 1.15, 0, 0], + "8747": [0.86225, 1.36, 0.44445, 0], + "8748": [0.862, 1.36, 0.44445, 0], + "8749": [0.862, 1.36, 0.44445, 0], + "8750": [0.86225, 1.36, 0.44445, 0], + "8896": [0.55001, 1.05, 0, 0], + "8897": [0.55001, 1.05, 0, 0], + "8898": [0.55001, 1.05, 0, 0], + "8899": [0.55001, 1.05, 0, 0], + "8968": [0.65002, 1.15, 0, 0], + "8969": [0.65002, 1.15, 0, 0], + "8970": [0.65002, 1.15, 0, 0], + "8971": [0.65002, 1.15, 0, 0], + "10216": [0.65002, 1.15, 0, 0], + "10217": [0.65002, 1.15, 0, 0], + "10752": [0.55001, 1.05, 0, 0], + "10753": [0.55001, 1.05, 0, 0], + "10754": [0.55001, 1.05, 0, 0], + "10756": [0.55001, 1.05, 0, 0], + "10758": [0.55001, 1.05, 0, 0], + }, + "Size3-Regular": { + "40": [0.95003, 1.45, 0, 0], + "41": [0.95003, 1.45, 0, 0], + "47": [0.95003, 1.45, 0, 0], + "91": [0.95003, 1.45, 0, 0], + "92": [0.95003, 1.45, 0, 0], + "93": [0.95003, 1.45, 0, 0], + "123": [0.95003, 1.45, 0, 0], + "125": [0.95003, 1.45, 0, 0], + "710": [0, 0.75, 0, 0], + "732": [0, 0.75, 0, 0], + "770": [0, 0.75, 0, 0], + "771": [0, 0.75, 0, 0], + "8730": [0.95003, 1.45, 0, 0], + "8968": [0.95003, 1.45, 0, 0], + "8969": [0.95003, 1.45, 0, 0], + "8970": [0.95003, 1.45, 0, 0], + "8971": [0.95003, 1.45, 0, 0], + "10216": [0.95003, 1.45, 0, 0], + "10217": [0.95003, 1.45, 0, 0], + }, + "Size4-Regular": { + "40": [1.25003, 1.75, 0, 0], + "41": [1.25003, 1.75, 0, 0], + "47": [1.25003, 1.75, 0, 0], + "91": [1.25003, 1.75, 0, 0], + "92": [1.25003, 1.75, 0, 0], + "93": [1.25003, 1.75, 0, 0], + "123": [1.25003, 1.75, 0, 0], + "125": [1.25003, 1.75, 0, 0], + "710": [0, 0.825, 0, 0], + "732": [0, 0.825, 0, 0], + "770": [0, 0.825, 0, 0], + "771": [0, 0.825, 0, 0], + "8730": [1.25003, 1.75, 0, 0], + "8968": [1.25003, 1.75, 0, 0], + "8969": [1.25003, 1.75, 0, 0], + "8970": [1.25003, 1.75, 0, 0], + "8971": [1.25003, 1.75, 0, 0], + "9115": [0.64502, 1.155, 0, 0], + "9116": [1e-05, 0.6, 0, 0], + "9117": [0.64502, 1.155, 0, 0], + "9118": [0.64502, 1.155, 0, 0], + "9119": [1e-05, 0.6, 0, 0], + "9120": [0.64502, 1.155, 0, 0], + "9121": [0.64502, 1.155, 0, 0], + "9122": [-0.00099, 0.601, 0, 0], + "9123": [0.64502, 1.155, 0, 0], + "9124": [0.64502, 1.155, 0, 0], + "9125": [-0.00099, 0.601, 0, 0], + "9126": [0.64502, 1.155, 0, 0], + "9127": [1e-05, 0.9, 0, 0], + "9128": [0.65002, 1.15, 0, 0], + "9129": [0.90001, 0, 0, 0], + "9130": [0, 0.3, 0, 0], + "9131": [1e-05, 0.9, 0, 0], + "9132": [0.65002, 1.15, 0, 0], + "9133": [0.90001, 0, 0, 0], + "9143": [0.88502, 0.915, 0, 0], + "10216": [1.25003, 1.75, 0, 0], + "10217": [1.25003, 1.75, 0, 0], + "57344": [-0.00499, 0.605, 0, 0], + "57345": [-0.00499, 0.605, 0, 0], + "57680": [0, 0.12, 0, 0], + "57681": [0, 0.12, 0, 0], + "57682": [0, 0.12, 0, 0], + "57683": [0, 0.12, 0, 0], + }, + "Typewriter-Regular": { + "33": [0, 0.61111, 0, 0], + "34": [0, 0.61111, 0, 0], + "35": [0, 0.61111, 0, 0], + "36": [0.08333, 0.69444, 0, 0], + "37": [0.08333, 0.69444, 0, 0], + "38": [0, 0.61111, 0, 0], + "39": [0, 0.61111, 0, 0], + "40": [0.08333, 0.69444, 0, 0], + "41": [0.08333, 0.69444, 0, 0], + "42": [0, 0.52083, 0, 0], + "43": [-0.08056, 0.53055, 0, 0], + "44": [0.13889, 0.125, 0, 0], + "45": [-0.08056, 0.53055, 0, 0], + "46": [0, 0.125, 0, 0], + "47": [0.08333, 0.69444, 0, 0], + "48": [0, 0.61111, 0, 0], + "49": [0, 0.61111, 0, 0], + "50": [0, 0.61111, 0, 0], + "51": [0, 0.61111, 0, 0], + "52": [0, 0.61111, 0, 0], + "53": [0, 0.61111, 0, 0], + "54": [0, 0.61111, 0, 0], + "55": [0, 0.61111, 0, 0], + "56": [0, 0.61111, 0, 0], + "57": [0, 0.61111, 0, 0], + "58": [0, 0.43056, 0, 0], + "59": [0.13889, 0.43056, 0, 0], + "60": [-0.05556, 0.55556, 0, 0], + "61": [-0.19549, 0.41562, 0, 0], + "62": [-0.05556, 0.55556, 0, 0], + "63": [0, 0.61111, 0, 0], + "64": [0, 0.61111, 0, 0], + "65": [0, 0.61111, 0, 0], + "66": [0, 0.61111, 0, 0], + "67": [0, 0.61111, 0, 0], + "68": [0, 0.61111, 0, 0], + "69": [0, 0.61111, 0, 0], + "70": [0, 0.61111, 0, 0], + "71": [0, 0.61111, 0, 0], + "72": [0, 0.61111, 0, 0], + "73": [0, 0.61111, 0, 0], + "74": [0, 0.61111, 0, 0], + "75": [0, 0.61111, 0, 0], + "76": [0, 0.61111, 0, 0], + "77": [0, 0.61111, 0, 0], + "78": [0, 0.61111, 0, 0], + "79": [0, 0.61111, 0, 0], + "80": [0, 0.61111, 0, 0], + "81": [0.13889, 0.61111, 0, 0], + "82": [0, 0.61111, 0, 0], + "83": [0, 0.61111, 0, 0], + "84": [0, 0.61111, 0, 0], + "85": [0, 0.61111, 0, 0], + "86": [0, 0.61111, 0, 0], + "87": [0, 0.61111, 0, 0], + "88": [0, 0.61111, 0, 0], + "89": [0, 0.61111, 0, 0], + "90": [0, 0.61111, 0, 0], + "91": [0.08333, 0.69444, 0, 0], + "92": [0.08333, 0.69444, 0, 0], + "93": [0.08333, 0.69444, 0, 0], + "94": [0, 0.61111, 0, 0], + "95": [0.09514, 0, 0, 0], + "96": [0, 0.61111, 0, 0], + "97": [0, 0.43056, 0, 0], + "98": [0, 0.61111, 0, 0], + "99": [0, 0.43056, 0, 0], + "100": [0, 0.61111, 0, 0], + "101": [0, 0.43056, 0, 0], + "102": [0, 0.61111, 0, 0], + "103": [0.22222, 0.43056, 0, 0], + "104": [0, 0.61111, 0, 0], + "105": [0, 0.61111, 0, 0], + "106": [0.22222, 0.61111, 0, 0], + "107": [0, 0.61111, 0, 0], + "108": [0, 0.61111, 0, 0], + "109": [0, 0.43056, 0, 0], + "110": [0, 0.43056, 0, 0], + "111": [0, 0.43056, 0, 0], + "112": [0.22222, 0.43056, 0, 0], + "113": [0.22222, 0.43056, 0, 0], + "114": [0, 0.43056, 0, 0], + "115": [0, 0.43056, 0, 0], + "116": [0, 0.55358, 0, 0], + "117": [0, 0.43056, 0, 0], + "118": [0, 0.43056, 0, 0], + "119": [0, 0.43056, 0, 0], + "120": [0, 0.43056, 0, 0], + "121": [0.22222, 0.43056, 0, 0], + "122": [0, 0.43056, 0, 0], + "123": [0.08333, 0.69444, 0, 0], + "124": [0.08333, 0.69444, 0, 0], + "125": [0.08333, 0.69444, 0, 0], + "126": [0, 0.61111, 0, 0], + "127": [0, 0.61111, 0, 0], + "305": [0, 0.43056, 0, 0], + "567": [0.22222, 0.43056, 0, 0], + "768": [0, 0.61111, 0, 0], + "769": [0, 0.61111, 0, 0], + "770": [0, 0.61111, 0, 0], + "771": [0, 0.61111, 0, 0], + "772": [0, 0.56555, 0, 0], + "774": [0, 0.61111, 0, 0], + "776": [0, 0.61111, 0, 0], + "778": [0, 0.61111, 0, 0], + "780": [0, 0.56597, 0, 0], + "915": [0, 0.61111, 0, 0], + "916": [0, 0.61111, 0, 0], + "920": [0, 0.61111, 0, 0], + "923": [0, 0.61111, 0, 0], + "926": [0, 0.61111, 0, 0], + "928": [0, 0.61111, 0, 0], + "931": [0, 0.61111, 0, 0], + "933": [0, 0.61111, 0, 0], + "934": [0, 0.61111, 0, 0], + "936": [0, 0.61111, 0, 0], + "937": [0, 0.61111, 0, 0], + "2018": [0, 0.61111, 0, 0], + "2019": [0, 0.61111, 0, 0], + "8242": [0, 0.61111, 0, 0], + }, +}; + +},{}],19:[function(require,module,exports){ +var utils = require("./utils"); +var ParseError = require("./ParseError"); + +/* This file contains a list of functions that we parse, identified by + * the calls to defineFunction. + * + * The first argument to defineFunction is a single name or a list of names. + * All functions named in such a list will share a single implementation. + * + * Each declared function can have associated properties, which + * include the following: + * + * - numArgs: The number of arguments the function takes. + * If this is the only property, it can be passed as a number + * instead of an element of a properties object. + * - argTypes: (optional) An array corresponding to each argument of the + * function, giving the type of argument that should be parsed. Its + * length should be equal to `numArgs + numOptionalArgs`. Valid + * types: + * - "size": A size-like thing, such as "1em" or "5ex" + * - "color": An html color, like "#abc" or "blue" + * - "original": The same type as the environment that the + * function being parsed is in (e.g. used for the + * bodies of functions like \color where the first + * argument is special and the second argument is + * parsed normally) + * Other possible types (probably shouldn't be used) + * - "text": Text-like (e.g. \text) + * - "math": Normal math + * If undefined, this will be treated as an appropriate length + * array of "original" strings + * - greediness: (optional) The greediness of the function to use ungrouped + * arguments. + * + * E.g. if you have an expression + * \sqrt \frac 1 2 + * since \frac has greediness=2 vs \sqrt's greediness=1, \frac + * will use the two arguments '1' and '2' as its two arguments, + * then that whole function will be used as the argument to + * \sqrt. On the other hand, the expressions + * \frac \frac 1 2 3 + * and + * \frac \sqrt 1 2 + * will fail because \frac and \frac have equal greediness + * and \sqrt has a lower greediness than \frac respectively. To + * make these parse, we would have to change them to: + * \frac {\frac 1 2} 3 + * and + * \frac {\sqrt 1} 2 + * + * The default value is `1` + * - allowedInText: (optional) Whether or not the function is allowed inside + * text mode (default false) + * - numOptionalArgs: (optional) The number of optional arguments the function + * should parse. If the optional arguments aren't found, + * `null` will be passed to the handler in their place. + * (default 0) + * - infix: (optional) Must be true if the function is an infix operator. + * + * The last argument is that implementation, the handler for the function(s). + * It is called to handle these functions and their arguments. + * It receives two arguments: + * - context contains information and references provided by the parser + * - args is an array of arguments obtained from TeX input + * The context contains the following properties: + * - funcName: the text (i.e. name) of the function, including \ + * - parser: the parser object + * - lexer: the lexer object + * - positions: the positions in the overall string of the function + * and the arguments. + * The latter three should only be used to produce error messages. + * + * The function should return an object with the following keys: + * - type: The type of element that this is. This is then used in + * buildHTML/buildMathML to determine which function + * should be called to build this node into a DOM node + * Any other data can be added to the object, which will be passed + * in to the function in buildHTML/buildMathML as `group.value`. + */ + +function defineFunction(names, props, handler) { + if (typeof names === "string") { + names = [names]; + } + if (typeof props === "number") { + props = { numArgs: props }; + } + // Set default values of functions + var data = { + numArgs: props.numArgs, + argTypes: props.argTypes, + greediness: (props.greediness === undefined) ? 1 : props.greediness, + allowedInText: !!props.allowedInText, + numOptionalArgs: props.numOptionalArgs || 0, + infix: !!props.infix, + handler: handler, + }; + for (var i = 0; i < names.length; ++i) { + module.exports[names[i]] = data; + } +} + +// A normal square root +defineFunction("\\sqrt", { + numArgs: 1, + numOptionalArgs: 1, +}, function(context, args) { + var index = args[0]; + var body = args[1]; + return { + type: "sqrt", + body: body, + index: index, + }; +}); + +// Some non-mathy text +defineFunction("\\text", { + numArgs: 1, + argTypes: ["text"], + greediness: 2, +}, function(context, args) { + var body = args[0]; + // Since the corresponding buildHTML/buildMathML function expects a + // list of elements, we normalize for different kinds of arguments + // TODO(emily): maybe this should be done somewhere else + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "text", + body: inner, + }; +}); + +// A two-argument custom color +defineFunction("\\color", { + numArgs: 2, + allowedInText: true, + greediness: 3, + argTypes: ["color", "original"], +}, function(context, args) { + var color = args[0]; + var body = args[1]; + // Normalize the different kinds of bodies (see \text above) + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "color", + color: color.value, + value: inner, + }; +}); + +// An overline +defineFunction("\\overline", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + return { + type: "overline", + body: body, + }; +}); + +// An underline +defineFunction("\\underline", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + return { + type: "underline", + body: body, + }; +}); + +// A box of the width and height +defineFunction("\\rule", { + numArgs: 2, + numOptionalArgs: 1, + argTypes: ["size", "size", "size"], +}, function(context, args) { + var shift = args[0]; + var width = args[1]; + var height = args[2]; + return { + type: "rule", + shift: shift && shift.value, + width: width.value, + height: height.value, + }; +}); + +defineFunction("\\kern", { + numArgs: 1, + argTypes: ["size"], +}, function(context, args) { + return { + type: "kern", + dimension: args[0].value, + }; +}); + +// A KaTeX logo +defineFunction("\\KaTeX", { + numArgs: 0, +}, function(context) { + return { + type: "katex", + }; +}); + +defineFunction("\\phantom", { + numArgs: 1, +}, function(context, args) { + var body = args[0]; + var inner; + if (body.type === "ordgroup") { + inner = body.value; + } else { + inner = [body]; + } + + return { + type: "phantom", + value: inner, + }; +}); + +// Extra data needed for the delimiter handler down below +var delimiterSizes = { + "\\bigl" : {type: "open", size: 1}, + "\\Bigl" : {type: "open", size: 2}, + "\\biggl": {type: "open", size: 3}, + "\\Biggl": {type: "open", size: 4}, + "\\bigr" : {type: "close", size: 1}, + "\\Bigr" : {type: "close", size: 2}, + "\\biggr": {type: "close", size: 3}, + "\\Biggr": {type: "close", size: 4}, + "\\bigm" : {type: "rel", size: 1}, + "\\Bigm" : {type: "rel", size: 2}, + "\\biggm": {type: "rel", size: 3}, + "\\Biggm": {type: "rel", size: 4}, + "\\big" : {type: "textord", size: 1}, + "\\Big" : {type: "textord", size: 2}, + "\\bigg" : {type: "textord", size: 3}, + "\\Bigg" : {type: "textord", size: 4}, +}; + +var delimiters = [ + "(", ")", "[", "\\lbrack", "]", "\\rbrack", + "\\{", "\\lbrace", "\\}", "\\rbrace", + "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", + "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt", + "\\lvert", "\\rvert", "\\lVert", "\\rVert", + "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache", + "/", "\\backslash", + "|", "\\vert", "\\|", "\\Vert", + "\\uparrow", "\\Uparrow", + "\\downarrow", "\\Downarrow", + "\\updownarrow", "\\Updownarrow", + ".", +]; + +var fontAliases = { + "\\Bbb": "\\mathbb", + "\\bold": "\\mathbf", + "\\frak": "\\mathfrak", +}; + +// Single-argument color functions +defineFunction([ + "\\blue", "\\orange", "\\pink", "\\red", + "\\green", "\\gray", "\\purple", + "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE", + "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE", + "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE", + "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE", + "\\redA", "\\redB", "\\redC", "\\redD", "\\redE", + "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE", + "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE", + "\\mintA", "\\mintB", "\\mintC", + "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE", + "\\grayF", "\\grayG", "\\grayH", "\\grayI", + "\\kaBlue", "\\kaGreen", +], { + numArgs: 1, + allowedInText: true, + greediness: 3, +}, function(context, args) { + var body = args[0]; + var atoms; + if (body.type === "ordgroup") { + atoms = body.value; + } else { + atoms = [body]; + } + + return { + type: "color", + color: "katex-" + context.funcName.slice(1), + value: atoms, + }; +}); + +// There are 2 flags for operators; whether they produce limits in +// displaystyle, and whether they are symbols and should grow in +// displaystyle. These four groups cover the four possible choices. + +// No limits, not symbols +defineFunction([ + "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh", + "\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom", + "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", + "\\tan", "\\tanh", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: false, + symbol: false, + body: context.funcName, + }; +}); + +// Limits, not symbols +defineFunction([ + "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max", + "\\min", "\\Pr", "\\sup", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: true, + symbol: false, + body: context.funcName, + }; +}); + +// No limits, symbols +defineFunction([ + "\\int", "\\iint", "\\iiint", "\\oint", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: false, + symbol: true, + body: context.funcName, + }; +}); + +// Limits, symbols +defineFunction([ + "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap", + "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes", + "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", +], { + numArgs: 0, +}, function(context) { + return { + type: "op", + limits: true, + symbol: true, + body: context.funcName, + }; +}); + +// Fractions +defineFunction([ + "\\dfrac", "\\frac", "\\tfrac", + "\\dbinom", "\\binom", "\\tbinom", +], { + numArgs: 2, + greediness: 2, +}, function(context, args) { + var numer = args[0]; + var denom = args[1]; + var hasBarLine; + var leftDelim = null; + var rightDelim = null; + var size = "auto"; + + switch (context.funcName) { + case "\\dfrac": + case "\\frac": + case "\\tfrac": + hasBarLine = true; + break; + case "\\dbinom": + case "\\binom": + case "\\tbinom": + hasBarLine = false; + leftDelim = "("; + rightDelim = ")"; + break; + default: + throw new Error("Unrecognized genfrac command"); + } + + switch (context.funcName) { + case "\\dfrac": + case "\\dbinom": + size = "display"; + break; + case "\\tfrac": + case "\\tbinom": + size = "text"; + break; + } + + return { + type: "genfrac", + numer: numer, + denom: denom, + hasBarLine: hasBarLine, + leftDelim: leftDelim, + rightDelim: rightDelim, + size: size, + }; +}); + +// Left and right overlap functions +defineFunction(["\\llap", "\\rlap"], { + numArgs: 1, + allowedInText: true, +}, function(context, args) { + var body = args[0]; + return { + type: context.funcName.slice(1), + body: body, + }; +}); + +// Delimiter functions +defineFunction([ + "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", + "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", + "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", + "\\big", "\\Big", "\\bigg", "\\Bigg", + "\\left", "\\right", +], { + numArgs: 1, +}, function(context, args) { + var delim = args[0]; + if (!utils.contains(delimiters, delim.value)) { + throw new ParseError( + "Invalid delimiter: '" + delim.value + "' after '" + + context.funcName + "'", delim); + } + + // \left and \right are caught somewhere in Parser.js, which is + // why this data doesn't match what is in buildHTML. + if (context.funcName === "\\left" || context.funcName === "\\right") { + return { + type: "leftright", + value: delim.value, + }; + } else { + return { + type: "delimsizing", + size: delimiterSizes[context.funcName].size, + delimType: delimiterSizes[context.funcName].type, + value: delim.value, + }; + } +}); + +// Sizing functions (handled in Parser.js explicitly, hence no handler) +defineFunction([ + "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", + "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", +], 0, null); + +// Style changing functions (handled in Parser.js explicitly, hence no +// handler) +defineFunction([ + "\\displaystyle", "\\textstyle", "\\scriptstyle", + "\\scriptscriptstyle", +], 0, null); + +defineFunction([ + // styles + "\\mathrm", "\\mathit", "\\mathbf", + + // families + "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", + "\\mathtt", + + // aliases + "\\Bbb", "\\bold", "\\frak", +], { + numArgs: 1, + greediness: 2, +}, function(context, args) { + var body = args[0]; + var func = context.funcName; + if (func in fontAliases) { + func = fontAliases[func]; + } + return { + type: "font", + font: func.slice(1), + body: body, + }; +}); + +// Accents +defineFunction([ + "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", + "\\check", "\\hat", "\\vec", "\\dot", + // We don't support expanding accents yet + // "\\widetilde", "\\widehat" +], { + numArgs: 1, +}, function(context, args) { + var base = args[0]; + return { + type: "accent", + accent: context.funcName, + base: base, + }; +}); + +// Infix generalized fractions +defineFunction(["\\over", "\\choose"], { + numArgs: 0, + infix: true, +}, function(context) { + var replaceWith; + switch (context.funcName) { + case "\\over": + replaceWith = "\\frac"; + break; + case "\\choose": + replaceWith = "\\binom"; + break; + default: + throw new Error("Unrecognized infix genfrac command"); + } + return { + type: "infix", + replaceWith: replaceWith, + token: context.token, + }; +}); + +// Row breaks for aligned data +defineFunction(["\\\\", "\\cr"], { + numArgs: 0, + numOptionalArgs: 1, + argTypes: ["size"], +}, function(context, args) { + var size = args[0]; + return { + type: "cr", + size: size, + }; +}); + +// Environment delimiters +defineFunction(["\\begin", "\\end"], { + numArgs: 1, + argTypes: ["text"], +}, function(context, args) { + var nameGroup = args[0]; + if (nameGroup.type !== "ordgroup") { + throw new ParseError("Invalid environment name", nameGroup); + } + var name = ""; + for (var i = 0; i < nameGroup.value.length; ++i) { + name += nameGroup.value[i].value; + } + return { + type: "environment", + name: name, + nameGroup: nameGroup, + }; +}); + +},{"./ParseError":6,"./utils":25}],20:[function(require,module,exports){ +/** + * These objects store data about MathML nodes. This is the MathML equivalent + * of the types in domTree.js. Since MathML handles its own rendering, and + * since we're mainly using MathML to improve accessibility, we don't manage + * any of the styling state that the plain DOM nodes do. + * + * The `toNode` and `toMarkup` functions work simlarly to how they do in + * domTree.js, creating namespaced DOM nodes and HTML text markup respectively. + */ + +var utils = require("./utils"); + +/** + * This node represents a general purpose MathML node of any type. The + * constructor requires the type of node to create (for example, `"mo"` or + * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags). + */ +function MathNode(type, children) { + this.type = type; + this.attributes = {}; + this.children = children || []; +} + +/** + * Sets an attribute on a MathML node. MathML depends on attributes to convey a + * semantic content, so this is used heavily. + */ +MathNode.prototype.setAttribute = function(name, value) { + this.attributes[name] = value; +}; + +/** + * Converts the math node into a MathML-namespaced DOM element. + */ +MathNode.prototype.toNode = function() { + var node = document.createElementNS( + "http://www.w3.org/1998/Math/MathML", this.type); + + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + node.setAttribute(attr, this.attributes[attr]); + } + } + + for (var i = 0; i < this.children.length; i++) { + node.appendChild(this.children[i].toNode()); + } + + return node; +}; + +/** + * Converts the math node into an HTML markup string. + */ +MathNode.prototype.toMarkup = function() { + var markup = "<" + this.type; + + // Add the attributes + for (var attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + for (var i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += "</" + this.type + ">"; + + return markup; +}; + +/** + * This node represents a piece of text. + */ +function TextNode(text) { + this.text = text; +} + +/** + * Converts the text node into a DOM text node. + */ +TextNode.prototype.toNode = function() { + return document.createTextNode(this.text); +}; + +/** + * Converts the text node into HTML markup (which is just the text itself). + */ +TextNode.prototype.toMarkup = function() { + return utils.escape(this.text); +}; + +module.exports = { + MathNode: MathNode, + TextNode: TextNode, +}; + +},{"./utils":25}],21:[function(require,module,exports){ +/** + * The resulting parse tree nodes of the parse tree. + * + * It is possible to provide position information, so that a ParseNode can + * fulfil a role similar to a Token in error reporting. + * For details on the corresponding properties see Token constructor. + * Providing such information can lead to better error reporting. + * + * @param {string} type type of node, like e.g. "ordgroup" + * @param {?object} value type-specific representation of the node + * @param {string} mode parse mode in action for this node, + * "math" or "text" + * @param {Token=} firstToken first token of the input for this node, + * will omit position information if unset + * @param {Token=} lastToken last token of the input for this node, + * will default to firstToken if unset + */ +function ParseNode(type, value, mode, firstToken, lastToken) { + this.type = type; + this.value = value; + this.mode = mode; + if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) { + this.lexer = firstToken.lexer; + this.start = firstToken.start; + this.end = (lastToken || firstToken).end; + } +} + +module.exports = { + ParseNode: ParseNode, +}; + + +},{}],22:[function(require,module,exports){ +/** + * Provides a single function for parsing an expression using a Parser + * TODO(emily): Remove this + */ + +var Parser = require("./Parser"); + +/** + * Parses an expression using a Parser, then returns the parsed result. + */ +var parseTree = function(toParse, settings) { + if (!(typeof toParse === 'string' || toParse instanceof String)) { + throw new TypeError('KaTeX can only parse string typed expression'); + } + var parser = new Parser(toParse, settings); + + return parser.parse(); +}; + +module.exports = parseTree; + +},{"./Parser":7}],23:[function(require,module,exports){ +/** + * This file holds a list of all no-argument functions and single-character + * symbols (like 'a' or ';'). + * + * For each of the symbols, there are three properties they can have: + * - font (required): the font to be used for this symbol. Either "main" (the + normal font), or "ams" (the ams fonts). + * - group (required): the ParseNode group type the symbol should have (i.e. + "textord", "mathord", etc). + See https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types + * - replace: the character that this symbol or function should be + * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi + * character in the main font). + * + * The outermost map in the table indicates what mode the symbols should be + * accepted in (e.g. "math" or "text"). + */ + +module.exports = { + math: {}, + text: {}, +}; + +function defineSymbol(mode, font, group, replace, name) { + module.exports[mode][name] = { + font: font, + group: group, + replace: replace, + }; +} + +// Some abbreviations for commonly used strings. +// This helps minify the code, and also spotting typos using jshint. + +// modes: +var math = "math"; +var text = "text"; + +// fonts: +var main = "main"; +var ams = "ams"; + +// groups: +var accent = "accent"; +var bin = "bin"; +var close = "close"; +var inner = "inner"; +var mathord = "mathord"; +var op = "op"; +var open = "open"; +var punct = "punct"; +var rel = "rel"; +var spacing = "spacing"; +var textord = "textord"; + +// Now comes the symbol table + +// Relation Symbols +defineSymbol(math, main, rel, "\u2261", "\\equiv"); +defineSymbol(math, main, rel, "\u227a", "\\prec"); +defineSymbol(math, main, rel, "\u227b", "\\succ"); +defineSymbol(math, main, rel, "\u223c", "\\sim"); +defineSymbol(math, main, rel, "\u22a5", "\\perp"); +defineSymbol(math, main, rel, "\u2aaf", "\\preceq"); +defineSymbol(math, main, rel, "\u2ab0", "\\succeq"); +defineSymbol(math, main, rel, "\u2243", "\\simeq"); +defineSymbol(math, main, rel, "\u2223", "\\mid"); +defineSymbol(math, main, rel, "\u226a", "\\ll"); +defineSymbol(math, main, rel, "\u226b", "\\gg"); +defineSymbol(math, main, rel, "\u224d", "\\asymp"); +defineSymbol(math, main, rel, "\u2225", "\\parallel"); +defineSymbol(math, main, rel, "\u22c8", "\\bowtie"); +defineSymbol(math, main, rel, "\u2323", "\\smile"); +defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq"); +defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq"); +defineSymbol(math, main, rel, "\u2250", "\\doteq"); +defineSymbol(math, main, rel, "\u2322", "\\frown"); +defineSymbol(math, main, rel, "\u220b", "\\ni"); +defineSymbol(math, main, rel, "\u221d", "\\propto"); +defineSymbol(math, main, rel, "\u22a2", "\\vdash"); +defineSymbol(math, main, rel, "\u22a3", "\\dashv"); +defineSymbol(math, main, rel, "\u220b", "\\owns"); + +// Punctuation +defineSymbol(math, main, punct, "\u002e", "\\ldotp"); +defineSymbol(math, main, punct, "\u22c5", "\\cdotp"); + +// Misc Symbols +defineSymbol(math, main, textord, "\u0023", "\\#"); +defineSymbol(math, main, textord, "\u0026", "\\&"); +defineSymbol(math, main, textord, "\u2135", "\\aleph"); +defineSymbol(math, main, textord, "\u2200", "\\forall"); +defineSymbol(math, main, textord, "\u210f", "\\hbar"); +defineSymbol(math, main, textord, "\u2203", "\\exists"); +defineSymbol(math, main, textord, "\u2207", "\\nabla"); +defineSymbol(math, main, textord, "\u266d", "\\flat"); +defineSymbol(math, main, textord, "\u2113", "\\ell"); +defineSymbol(math, main, textord, "\u266e", "\\natural"); +defineSymbol(math, main, textord, "\u2663", "\\clubsuit"); +defineSymbol(math, main, textord, "\u2118", "\\wp"); +defineSymbol(math, main, textord, "\u266f", "\\sharp"); +defineSymbol(math, main, textord, "\u2662", "\\diamondsuit"); +defineSymbol(math, main, textord, "\u211c", "\\Re"); +defineSymbol(math, main, textord, "\u2661", "\\heartsuit"); +defineSymbol(math, main, textord, "\u2111", "\\Im"); +defineSymbol(math, main, textord, "\u2660", "\\spadesuit"); + +// Math and Text +defineSymbol(math, main, textord, "\u2020", "\\dag"); +defineSymbol(math, main, textord, "\u2021", "\\ddag"); + +// Large Delimiters +defineSymbol(math, main, close, "\u23b1", "\\rmoustache"); +defineSymbol(math, main, open, "\u23b0", "\\lmoustache"); +defineSymbol(math, main, close, "\u27ef", "\\rgroup"); +defineSymbol(math, main, open, "\u27ee", "\\lgroup"); + +// Binary Operators +defineSymbol(math, main, bin, "\u2213", "\\mp"); +defineSymbol(math, main, bin, "\u2296", "\\ominus"); +defineSymbol(math, main, bin, "\u228e", "\\uplus"); +defineSymbol(math, main, bin, "\u2293", "\\sqcap"); +defineSymbol(math, main, bin, "\u2217", "\\ast"); +defineSymbol(math, main, bin, "\u2294", "\\sqcup"); +defineSymbol(math, main, bin, "\u25ef", "\\bigcirc"); +defineSymbol(math, main, bin, "\u2219", "\\bullet"); +defineSymbol(math, main, bin, "\u2021", "\\ddagger"); +defineSymbol(math, main, bin, "\u2240", "\\wr"); +defineSymbol(math, main, bin, "\u2a3f", "\\amalg"); + +// Arrow Symbols +defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow"); +defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow"); +defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow"); +defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow"); +defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow"); +defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow"); +defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow"); +defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow"); +defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow"); +defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow"); +defineSymbol(math, main, rel, "\u21a6", "\\mapsto"); +defineSymbol(math, main, rel, "\u27fc", "\\longmapsto"); +defineSymbol(math, main, rel, "\u2197", "\\nearrow"); +defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow"); +defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow"); +defineSymbol(math, main, rel, "\u2198", "\\searrow"); +defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup"); +defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup"); +defineSymbol(math, main, rel, "\u2199", "\\swarrow"); +defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown"); +defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown"); +defineSymbol(math, main, rel, "\u2196", "\\nwarrow"); +defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons"); + +// AMS Negated Binary Relations +defineSymbol(math, ams, rel, "\u226e", "\\nless"); +defineSymbol(math, ams, rel, "\ue010", "\\nleqslant"); +defineSymbol(math, ams, rel, "\ue011", "\\nleqq"); +defineSymbol(math, ams, rel, "\u2a87", "\\lneq"); +defineSymbol(math, ams, rel, "\u2268", "\\lneqq"); +defineSymbol(math, ams, rel, "\ue00c", "\\lvertneqq"); +defineSymbol(math, ams, rel, "\u22e6", "\\lnsim"); +defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox"); +defineSymbol(math, ams, rel, "\u2280", "\\nprec"); +defineSymbol(math, ams, rel, "\u22e0", "\\npreceq"); +defineSymbol(math, ams, rel, "\u22e8", "\\precnsim"); +defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox"); +defineSymbol(math, ams, rel, "\u2241", "\\nsim"); +defineSymbol(math, ams, rel, "\ue006", "\\nshortmid"); +defineSymbol(math, ams, rel, "\u2224", "\\nmid"); +defineSymbol(math, ams, rel, "\u22ac", "\\nvdash"); +defineSymbol(math, ams, rel, "\u22ad", "\\nvDash"); +defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft"); +defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq"); +defineSymbol(math, ams, rel, "\u228a", "\\subsetneq"); +defineSymbol(math, ams, rel, "\ue01a", "\\varsubsetneq"); +defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq"); +defineSymbol(math, ams, rel, "\ue017", "\\varsubsetneqq"); +defineSymbol(math, ams, rel, "\u226f", "\\ngtr"); +defineSymbol(math, ams, rel, "\ue00f", "\\ngeqslant"); +defineSymbol(math, ams, rel, "\ue00e", "\\ngeqq"); +defineSymbol(math, ams, rel, "\u2a88", "\\gneq"); +defineSymbol(math, ams, rel, "\u2269", "\\gneqq"); +defineSymbol(math, ams, rel, "\ue00d", "\\gvertneqq"); +defineSymbol(math, ams, rel, "\u22e7", "\\gnsim"); +defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox"); +defineSymbol(math, ams, rel, "\u2281", "\\nsucc"); +defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq"); +defineSymbol(math, ams, rel, "\u22e9", "\\succnsim"); +defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox"); +defineSymbol(math, ams, rel, "\u2246", "\\ncong"); +defineSymbol(math, ams, rel, "\ue007", "\\nshortparallel"); +defineSymbol(math, ams, rel, "\u2226", "\\nparallel"); +defineSymbol(math, ams, rel, "\u22af", "\\nVDash"); +defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright"); +defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq"); +defineSymbol(math, ams, rel, "\ue018", "\\nsupseteqq"); +defineSymbol(math, ams, rel, "\u228b", "\\supsetneq"); +defineSymbol(math, ams, rel, "\ue01b", "\\varsupsetneq"); +defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq"); +defineSymbol(math, ams, rel, "\ue019", "\\varsupsetneqq"); +defineSymbol(math, ams, rel, "\u22ae", "\\nVdash"); +defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq"); +defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq"); +defineSymbol(math, ams, rel, "\ue016", "\\nsubseteqq"); +defineSymbol(math, ams, bin, "\u22b4", "\\unlhd"); +defineSymbol(math, ams, bin, "\u22b5", "\\unrhd"); + +// AMS Negated Arrows +defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow"); +defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow"); +defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow"); +defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow"); +defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow"); +defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow"); + +// AMS Misc +defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle"); +defineSymbol(math, ams, textord, "\u210f", "\\hslash"); +defineSymbol(math, ams, textord, "\u25bd", "\\triangledown"); +defineSymbol(math, ams, textord, "\u25ca", "\\lozenge"); +defineSymbol(math, ams, textord, "\u24c8", "\\circledS"); +defineSymbol(math, ams, textord, "\u00ae", "\\circledR"); +defineSymbol(math, ams, textord, "\u2221", "\\measuredangle"); +defineSymbol(math, ams, textord, "\u2204", "\\nexists"); +defineSymbol(math, ams, textord, "\u2127", "\\mho"); +defineSymbol(math, ams, textord, "\u2132", "\\Finv"); +defineSymbol(math, ams, textord, "\u2141", "\\Game"); +defineSymbol(math, ams, textord, "\u006b", "\\Bbbk"); +defineSymbol(math, ams, textord, "\u2035", "\\backprime"); +defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle"); +defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown"); +defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare"); +defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge"); +defineSymbol(math, ams, textord, "\u2605", "\\bigstar"); +defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle"); +defineSymbol(math, ams, textord, "\u2201", "\\complement"); +defineSymbol(math, ams, textord, "\u00f0", "\\eth"); +defineSymbol(math, ams, textord, "\u2571", "\\diagup"); +defineSymbol(math, ams, textord, "\u2572", "\\diagdown"); +defineSymbol(math, ams, textord, "\u25a1", "\\square"); +defineSymbol(math, ams, textord, "\u25a1", "\\Box"); +defineSymbol(math, ams, textord, "\u25ca", "\\Diamond"); +defineSymbol(math, ams, textord, "\u00a5", "\\yen"); +defineSymbol(math, ams, textord, "\u2713", "\\checkmark"); + +// AMS Hebrew +defineSymbol(math, ams, textord, "\u2136", "\\beth"); +defineSymbol(math, ams, textord, "\u2138", "\\daleth"); +defineSymbol(math, ams, textord, "\u2137", "\\gimel"); + +// AMS Greek +defineSymbol(math, ams, textord, "\u03dd", "\\digamma"); +defineSymbol(math, ams, textord, "\u03f0", "\\varkappa"); + +// AMS Delimiters +defineSymbol(math, ams, open, "\u250c", "\\ulcorner"); +defineSymbol(math, ams, close, "\u2510", "\\urcorner"); +defineSymbol(math, ams, open, "\u2514", "\\llcorner"); +defineSymbol(math, ams, close, "\u2518", "\\lrcorner"); + +// AMS Binary Relations +defineSymbol(math, ams, rel, "\u2266", "\\leqq"); +defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant"); +defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless"); +defineSymbol(math, ams, rel, "\u2272", "\\lesssim"); +defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox"); +defineSymbol(math, ams, rel, "\u224a", "\\approxeq"); +defineSymbol(math, ams, bin, "\u22d6", "\\lessdot"); +defineSymbol(math, ams, rel, "\u22d8", "\\lll"); +defineSymbol(math, ams, rel, "\u2276", "\\lessgtr"); +defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr"); +defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr"); +defineSymbol(math, ams, rel, "\u2251", "\\doteqdot"); +defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq"); +defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq"); +defineSymbol(math, ams, rel, "\u223d", "\\backsim"); +defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq"); +defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq"); +defineSymbol(math, ams, rel, "\u22d0", "\\Subset"); +defineSymbol(math, ams, rel, "\u228f", "\\sqsubset"); +defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq"); +defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec"); +defineSymbol(math, ams, rel, "\u227e", "\\precsim"); +defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox"); +defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft"); +defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq"); +defineSymbol(math, ams, rel, "\u22a8", "\\vDash"); +defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash"); +defineSymbol(math, ams, rel, "\u2323", "\\smallsmile"); +defineSymbol(math, ams, rel, "\u2322", "\\smallfrown"); +defineSymbol(math, ams, rel, "\u224f", "\\bumpeq"); +defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq"); +defineSymbol(math, ams, rel, "\u2267", "\\geqq"); +defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant"); +defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr"); +defineSymbol(math, ams, rel, "\u2273", "\\gtrsim"); +defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox"); +defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot"); +defineSymbol(math, ams, rel, "\u22d9", "\\ggg"); +defineSymbol(math, ams, rel, "\u2277", "\\gtrless"); +defineSymbol(math, ams, rel, "\u22db", "\\gtreqless"); +defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless"); +defineSymbol(math, ams, rel, "\u2256", "\\eqcirc"); +defineSymbol(math, ams, rel, "\u2257", "\\circeq"); +defineSymbol(math, ams, rel, "\u225c", "\\triangleq"); +defineSymbol(math, ams, rel, "\u223c", "\\thicksim"); +defineSymbol(math, ams, rel, "\u2248", "\\thickapprox"); +defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq"); +defineSymbol(math, ams, rel, "\u22d1", "\\Supset"); +defineSymbol(math, ams, rel, "\u2290", "\\sqsupset"); +defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq"); +defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc"); +defineSymbol(math, ams, rel, "\u227f", "\\succsim"); +defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox"); +defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright"); +defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq"); +defineSymbol(math, ams, rel, "\u22a9", "\\Vdash"); +defineSymbol(math, ams, rel, "\u2223", "\\shortmid"); +defineSymbol(math, ams, rel, "\u2225", "\\shortparallel"); +defineSymbol(math, ams, rel, "\u226c", "\\between"); +defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork"); +defineSymbol(math, ams, rel, "\u221d", "\\varpropto"); +defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft"); +defineSymbol(math, ams, rel, "\u2234", "\\therefore"); +defineSymbol(math, ams, rel, "\u220d", "\\backepsilon"); +defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright"); +defineSymbol(math, ams, rel, "\u2235", "\\because"); +defineSymbol(math, ams, rel, "\u22d8", "\\llless"); +defineSymbol(math, ams, rel, "\u22d9", "\\gggtr"); +defineSymbol(math, ams, bin, "\u22b2", "\\lhd"); +defineSymbol(math, ams, bin, "\u22b3", "\\rhd"); +defineSymbol(math, ams, rel, "\u2242", "\\eqsim"); +defineSymbol(math, main, rel, "\u22c8", "\\Join"); +defineSymbol(math, ams, rel, "\u2251", "\\Doteq"); + +// AMS Binary Operators +defineSymbol(math, ams, bin, "\u2214", "\\dotplus"); +defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus"); +defineSymbol(math, ams, bin, "\u22d2", "\\Cap"); +defineSymbol(math, ams, bin, "\u22d3", "\\Cup"); +defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge"); +defineSymbol(math, ams, bin, "\u229f", "\\boxminus"); +defineSymbol(math, ams, bin, "\u229e", "\\boxplus"); +defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes"); +defineSymbol(math, ams, bin, "\u22c9", "\\ltimes"); +defineSymbol(math, ams, bin, "\u22ca", "\\rtimes"); +defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes"); +defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes"); +defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge"); +defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee"); +defineSymbol(math, ams, bin, "\u229d", "\\circleddash"); +defineSymbol(math, ams, bin, "\u229b", "\\circledast"); +defineSymbol(math, ams, bin, "\u22c5", "\\centerdot"); +defineSymbol(math, ams, bin, "\u22ba", "\\intercal"); +defineSymbol(math, ams, bin, "\u22d2", "\\doublecap"); +defineSymbol(math, ams, bin, "\u22d3", "\\doublecup"); +defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes"); + +// AMS Arrows +defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow"); +defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow"); +defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows"); +defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows"); +defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow"); +defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow"); +defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail"); +defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft"); +defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons"); +defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft"); +defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft"); +defineSymbol(math, ams, rel, "\u21b0", "\\Lsh"); +defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows"); +defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft"); +defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft"); +defineSymbol(math, ams, rel, "\u22b8", "\\multimap"); +defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow"); +defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows"); +defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows"); +defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow"); +defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail"); +defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright"); +defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright"); +defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright"); +defineSymbol(math, ams, rel, "\u21b1", "\\Rsh"); +defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows"); +defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright"); +defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright"); +defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow"); +defineSymbol(math, ams, rel, "\u21dd", "\\leadsto"); +defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow"); +defineSymbol(math, ams, rel, "\u21be", "\\restriction"); + +defineSymbol(math, main, textord, "\u2018", "`"); +defineSymbol(math, main, textord, "$", "\\$"); +defineSymbol(math, main, textord, "%", "\\%"); +defineSymbol(math, main, textord, "_", "\\_"); +defineSymbol(math, main, textord, "\u2220", "\\angle"); +defineSymbol(math, main, textord, "\u221e", "\\infty"); +defineSymbol(math, main, textord, "\u2032", "\\prime"); +defineSymbol(math, main, textord, "\u25b3", "\\triangle"); +defineSymbol(math, main, textord, "\u0393", "\\Gamma"); +defineSymbol(math, main, textord, "\u0394", "\\Delta"); +defineSymbol(math, main, textord, "\u0398", "\\Theta"); +defineSymbol(math, main, textord, "\u039b", "\\Lambda"); +defineSymbol(math, main, textord, "\u039e", "\\Xi"); +defineSymbol(math, main, textord, "\u03a0", "\\Pi"); +defineSymbol(math, main, textord, "\u03a3", "\\Sigma"); +defineSymbol(math, main, textord, "\u03a5", "\\Upsilon"); +defineSymbol(math, main, textord, "\u03a6", "\\Phi"); +defineSymbol(math, main, textord, "\u03a8", "\\Psi"); +defineSymbol(math, main, textord, "\u03a9", "\\Omega"); +defineSymbol(math, main, textord, "\u00ac", "\\neg"); +defineSymbol(math, main, textord, "\u00ac", "\\lnot"); +defineSymbol(math, main, textord, "\u22a4", "\\top"); +defineSymbol(math, main, textord, "\u22a5", "\\bot"); +defineSymbol(math, main, textord, "\u2205", "\\emptyset"); +defineSymbol(math, ams, textord, "\u2205", "\\varnothing"); +defineSymbol(math, main, mathord, "\u03b1", "\\alpha"); +defineSymbol(math, main, mathord, "\u03b2", "\\beta"); +defineSymbol(math, main, mathord, "\u03b3", "\\gamma"); +defineSymbol(math, main, mathord, "\u03b4", "\\delta"); +defineSymbol(math, main, mathord, "\u03f5", "\\epsilon"); +defineSymbol(math, main, mathord, "\u03b6", "\\zeta"); +defineSymbol(math, main, mathord, "\u03b7", "\\eta"); +defineSymbol(math, main, mathord, "\u03b8", "\\theta"); +defineSymbol(math, main, mathord, "\u03b9", "\\iota"); +defineSymbol(math, main, mathord, "\u03ba", "\\kappa"); +defineSymbol(math, main, mathord, "\u03bb", "\\lambda"); +defineSymbol(math, main, mathord, "\u03bc", "\\mu"); +defineSymbol(math, main, mathord, "\u03bd", "\\nu"); +defineSymbol(math, main, mathord, "\u03be", "\\xi"); +defineSymbol(math, main, mathord, "o", "\\omicron"); +defineSymbol(math, main, mathord, "\u03c0", "\\pi"); +defineSymbol(math, main, mathord, "\u03c1", "\\rho"); +defineSymbol(math, main, mathord, "\u03c3", "\\sigma"); +defineSymbol(math, main, mathord, "\u03c4", "\\tau"); +defineSymbol(math, main, mathord, "\u03c5", "\\upsilon"); +defineSymbol(math, main, mathord, "\u03d5", "\\phi"); +defineSymbol(math, main, mathord, "\u03c7", "\\chi"); +defineSymbol(math, main, mathord, "\u03c8", "\\psi"); +defineSymbol(math, main, mathord, "\u03c9", "\\omega"); +defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon"); +defineSymbol(math, main, mathord, "\u03d1", "\\vartheta"); +defineSymbol(math, main, mathord, "\u03d6", "\\varpi"); +defineSymbol(math, main, mathord, "\u03f1", "\\varrho"); +defineSymbol(math, main, mathord, "\u03c2", "\\varsigma"); +defineSymbol(math, main, mathord, "\u03c6", "\\varphi"); +defineSymbol(math, main, bin, "\u2217", "*"); +defineSymbol(math, main, bin, "+", "+"); +defineSymbol(math, main, bin, "\u2212", "-"); +defineSymbol(math, main, bin, "\u22c5", "\\cdot"); +defineSymbol(math, main, bin, "\u2218", "\\circ"); +defineSymbol(math, main, bin, "\u00f7", "\\div"); +defineSymbol(math, main, bin, "\u00b1", "\\pm"); +defineSymbol(math, main, bin, "\u00d7", "\\times"); +defineSymbol(math, main, bin, "\u2229", "\\cap"); +defineSymbol(math, main, bin, "\u222a", "\\cup"); +defineSymbol(math, main, bin, "\u2216", "\\setminus"); +defineSymbol(math, main, bin, "\u2227", "\\land"); +defineSymbol(math, main, bin, "\u2228", "\\lor"); +defineSymbol(math, main, bin, "\u2227", "\\wedge"); +defineSymbol(math, main, bin, "\u2228", "\\vee"); +defineSymbol(math, main, textord, "\u221a", "\\surd"); +defineSymbol(math, main, open, "(", "("); +defineSymbol(math, main, open, "[", "["); +defineSymbol(math, main, open, "\u27e8", "\\langle"); +defineSymbol(math, main, open, "\u2223", "\\lvert"); +defineSymbol(math, main, open, "\u2225", "\\lVert"); +defineSymbol(math, main, close, ")", ")"); +defineSymbol(math, main, close, "]", "]"); +defineSymbol(math, main, close, "?", "?"); +defineSymbol(math, main, close, "!", "!"); +defineSymbol(math, main, close, "\u27e9", "\\rangle"); +defineSymbol(math, main, close, "\u2223", "\\rvert"); +defineSymbol(math, main, close, "\u2225", "\\rVert"); +defineSymbol(math, main, rel, "=", "="); +defineSymbol(math, main, rel, "<", "<"); +defineSymbol(math, main, rel, ">", ">"); +defineSymbol(math, main, rel, ":", ":"); +defineSymbol(math, main, rel, "\u2248", "\\approx"); +defineSymbol(math, main, rel, "\u2245", "\\cong"); +defineSymbol(math, main, rel, "\u2265", "\\ge"); +defineSymbol(math, main, rel, "\u2265", "\\geq"); +defineSymbol(math, main, rel, "\u2190", "\\gets"); +defineSymbol(math, main, rel, ">", "\\gt"); +defineSymbol(math, main, rel, "\u2208", "\\in"); +defineSymbol(math, main, rel, "\u2209", "\\notin"); +defineSymbol(math, main, rel, "\u2282", "\\subset"); +defineSymbol(math, main, rel, "\u2283", "\\supset"); +defineSymbol(math, main, rel, "\u2286", "\\subseteq"); +defineSymbol(math, main, rel, "\u2287", "\\supseteq"); +defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq"); +defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq"); +defineSymbol(math, main, rel, "\u22a8", "\\models"); +defineSymbol(math, main, rel, "\u2190", "\\leftarrow"); +defineSymbol(math, main, rel, "\u2264", "\\le"); +defineSymbol(math, main, rel, "\u2264", "\\leq"); +defineSymbol(math, main, rel, "<", "\\lt"); +defineSymbol(math, main, rel, "\u2260", "\\ne"); +defineSymbol(math, main, rel, "\u2260", "\\neq"); +defineSymbol(math, main, rel, "\u2192", "\\rightarrow"); +defineSymbol(math, main, rel, "\u2192", "\\to"); +defineSymbol(math, ams, rel, "\u2271", "\\ngeq"); +defineSymbol(math, ams, rel, "\u2270", "\\nleq"); +defineSymbol(math, main, spacing, null, "\\!"); +defineSymbol(math, main, spacing, "\u00a0", "\\ "); +defineSymbol(math, main, spacing, "\u00a0", "~"); +defineSymbol(math, main, spacing, null, "\\,"); +defineSymbol(math, main, spacing, null, "\\:"); +defineSymbol(math, main, spacing, null, "\\;"); +defineSymbol(math, main, spacing, null, "\\enspace"); +defineSymbol(math, main, spacing, null, "\\qquad"); +defineSymbol(math, main, spacing, null, "\\quad"); +defineSymbol(math, main, spacing, "\u00a0", "\\space"); +defineSymbol(math, main, punct, ",", ","); +defineSymbol(math, main, punct, ";", ";"); +defineSymbol(math, main, punct, ":", "\\colon"); +defineSymbol(math, ams, bin, "\u22bc", "\\barwedge"); +defineSymbol(math, ams, bin, "\u22bb", "\\veebar"); +defineSymbol(math, main, bin, "\u2299", "\\odot"); +defineSymbol(math, main, bin, "\u2295", "\\oplus"); +defineSymbol(math, main, bin, "\u2297", "\\otimes"); +defineSymbol(math, main, textord, "\u2202", "\\partial"); +defineSymbol(math, main, bin, "\u2298", "\\oslash"); +defineSymbol(math, ams, bin, "\u229a", "\\circledcirc"); +defineSymbol(math, ams, bin, "\u22a1", "\\boxdot"); +defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup"); +defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown"); +defineSymbol(math, main, bin, "\u2020", "\\dagger"); +defineSymbol(math, main, bin, "\u22c4", "\\diamond"); +defineSymbol(math, main, bin, "\u22c6", "\\star"); +defineSymbol(math, main, bin, "\u25c3", "\\triangleleft"); +defineSymbol(math, main, bin, "\u25b9", "\\triangleright"); +defineSymbol(math, main, open, "{", "\\{"); +defineSymbol(math, main, close, "}", "\\}"); +defineSymbol(math, main, open, "{", "\\lbrace"); +defineSymbol(math, main, close, "}", "\\rbrace"); +defineSymbol(math, main, open, "[", "\\lbrack"); +defineSymbol(math, main, close, "]", "\\rbrack"); +defineSymbol(math, main, open, "\u230a", "\\lfloor"); +defineSymbol(math, main, close, "\u230b", "\\rfloor"); +defineSymbol(math, main, open, "\u2308", "\\lceil"); +defineSymbol(math, main, close, "\u2309", "\\rceil"); +defineSymbol(math, main, textord, "\\", "\\backslash"); +defineSymbol(math, main, textord, "\u2223", "|"); +defineSymbol(math, main, textord, "\u2223", "\\vert"); +defineSymbol(math, main, textord, "\u2225", "\\|"); +defineSymbol(math, main, textord, "\u2225", "\\Vert"); +defineSymbol(math, main, rel, "\u2191", "\\uparrow"); +defineSymbol(math, main, rel, "\u21d1", "\\Uparrow"); +defineSymbol(math, main, rel, "\u2193", "\\downarrow"); +defineSymbol(math, main, rel, "\u21d3", "\\Downarrow"); +defineSymbol(math, main, rel, "\u2195", "\\updownarrow"); +defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow"); +defineSymbol(math, math, op, "\u2210", "\\coprod"); +defineSymbol(math, math, op, "\u22c1", "\\bigvee"); +defineSymbol(math, math, op, "\u22c0", "\\bigwedge"); +defineSymbol(math, math, op, "\u2a04", "\\biguplus"); +defineSymbol(math, math, op, "\u22c2", "\\bigcap"); +defineSymbol(math, math, op, "\u22c3", "\\bigcup"); +defineSymbol(math, math, op, "\u222b", "\\int"); +defineSymbol(math, math, op, "\u222b", "\\intop"); +defineSymbol(math, math, op, "\u222c", "\\iint"); +defineSymbol(math, math, op, "\u222d", "\\iiint"); +defineSymbol(math, math, op, "\u220f", "\\prod"); +defineSymbol(math, math, op, "\u2211", "\\sum"); +defineSymbol(math, math, op, "\u2a02", "\\bigotimes"); +defineSymbol(math, math, op, "\u2a01", "\\bigoplus"); +defineSymbol(math, math, op, "\u2a00", "\\bigodot"); +defineSymbol(math, math, op, "\u222e", "\\oint"); +defineSymbol(math, math, op, "\u2a06", "\\bigsqcup"); +defineSymbol(math, math, op, "\u222b", "\\smallint"); +defineSymbol(math, main, inner, "\u2026", "\\ldots"); +defineSymbol(math, main, inner, "\u22ef", "\\cdots"); +defineSymbol(math, main, inner, "\u22f1", "\\ddots"); +defineSymbol(math, main, textord, "\u22ee", "\\vdots"); +defineSymbol(math, main, accent, "\u00b4", "\\acute"); +defineSymbol(math, main, accent, "\u0060", "\\grave"); +defineSymbol(math, main, accent, "\u00a8", "\\ddot"); +defineSymbol(math, main, accent, "\u007e", "\\tilde"); +defineSymbol(math, main, accent, "\u00af", "\\bar"); +defineSymbol(math, main, accent, "\u02d8", "\\breve"); +defineSymbol(math, main, accent, "\u02c7", "\\check"); +defineSymbol(math, main, accent, "\u005e", "\\hat"); +defineSymbol(math, main, accent, "\u20d7", "\\vec"); +defineSymbol(math, main, accent, "\u02d9", "\\dot"); +defineSymbol(math, main, mathord, "\u0131", "\\imath"); +defineSymbol(math, main, mathord, "\u0237", "\\jmath"); + +defineSymbol(text, main, textord, "\u2013", "--"); +defineSymbol(text, main, textord, "\u2014", "---"); +defineSymbol(text, main, textord, "\u2018", "`"); +defineSymbol(text, main, textord, "\u2019", "'"); +defineSymbol(text, main, textord, "\u201c", "``"); +defineSymbol(text, main, textord, "\u201d", "''"); +defineSymbol(math, main, textord, "\u00b0", "\\degree"); +defineSymbol(text, main, textord, "\u00b0", "\\degree"); +defineSymbol(math, main, mathord, "\u00a3", "\\pounds"); +defineSymbol(math, ams, textord, "\u2720", "\\maltese"); +defineSymbol(text, ams, textord, "\u2720", "\\maltese"); + +defineSymbol(text, main, spacing, "\u00a0", "\\ "); +defineSymbol(text, main, spacing, "\u00a0", " "); +defineSymbol(text, main, spacing, "\u00a0", "~"); + +// There are lots of symbols which are the same, so we add them in afterwards. +var i; +var ch; + +// All of these are textords in math mode +var mathTextSymbols = "0123456789/@.\""; +for (i = 0; i < mathTextSymbols.length; i++) { + ch = mathTextSymbols.charAt(i); + defineSymbol(math, main, textord, ch, ch); +} + +// All of these are textords in text mode +var textSymbols = "0123456789!@*()-=+[]\";:?/.,"; +for (i = 0; i < textSymbols.length; i++) { + ch = textSymbols.charAt(i); + defineSymbol(text, main, textord, ch, ch); +} + +// All of these are textords in text mode, and mathords in math mode +var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +for (i = 0; i < letters.length; i++) { + ch = letters.charAt(i); + defineSymbol(math, main, mathord, ch, ch); + defineSymbol(text, main, textord, ch, ch); +} + +// Latin-1 letters +for (i = 0x00C0; i <= 0x00D6; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +for (i = 0x00D8; i <= 0x00F6; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +for (i = 0x00F8; i <= 0x00FF; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +// Cyrillic +for (i = 0x0410; i <= 0x044F; i++) { + ch = String.fromCharCode(i); + defineSymbol(text, main, textord, ch, ch); +} + +},{}],24:[function(require,module,exports){ +var hangulRegex = /[\uAC00-\uD7AF]/; + +// This regex combines +// - Hiragana: [\u3040-\u309F] +// - Katakana: [\u30A0-\u30FF] +// - CJK ideograms: [\u4E00-\u9FAF] +// - Hangul syllables: [\uAC00-\uD7AF] +// Notably missing are halfwidth Katakana and Romanji glyphs. +var cjkRegex = + /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/; + +module.exports = { + cjkRegex: cjkRegex, + hangulRegex: hangulRegex, +}; + +},{}],25:[function(require,module,exports){ +/** + * This file contains a list of utility functions which are useful in other + * files. + */ + +/** + * Provide an `indexOf` function which works in IE8, but defers to native if + * possible. + */ +var nativeIndexOf = Array.prototype.indexOf; +var indexOf = function(list, elem) { + if (list == null) { + return -1; + } + if (nativeIndexOf && list.indexOf === nativeIndexOf) { + return list.indexOf(elem); + } + var i = 0; + var l = list.length; + for (; i < l; i++) { + if (list[i] === elem) { + return i; + } + } + return -1; +}; + +/** + * Return whether an element is contained in a list + */ +var contains = function(list, elem) { + return indexOf(list, elem) !== -1; +}; + +/** + * Provide a default value if a setting is undefined + */ +var deflt = function(setting, defaultIfUndefined) { + return setting === undefined ? defaultIfUndefined : setting; +}; + +// hyphenate and escape adapted from Facebook's React under Apache 2 license + +var uppercase = /([A-Z])/g; +var hyphenate = function(str) { + return str.replace(uppercase, "-$1").toLowerCase(); +}; + +var ESCAPE_LOOKUP = { + "&": "&", + ">": ">", + "<": "<", + "\"": """, + "'": "'", +}; + +var ESCAPE_REGEX = /[&><"']/g; + +function escaper(match) { + return ESCAPE_LOOKUP[match]; +} + +/** + * Escapes text to prevent scripting attacks. + * + * @param {*} text Text value to escape. + * @return {string} An escaped string. + */ +function escape(text) { + return ("" + text).replace(ESCAPE_REGEX, escaper); +} + +/** + * A function to set the text content of a DOM element in all supported + * browsers. Note that we don't define this if there is no document. + */ +var setTextContent; +if (typeof document !== "undefined") { + var testNode = document.createElement("span"); + if ("textContent" in testNode) { + setTextContent = function(node, text) { + node.textContent = text; + }; + } else { + setTextContent = function(node, text) { + node.innerText = text; + }; + } +} + +/** + * A function to clear a node. + */ +function clearNode(node) { + setTextContent(node, ""); +} + +module.exports = { + contains: contains, + deflt: deflt, + escape: escape, + hyphenate: hyphenate, + indexOf: indexOf, + setTextContent: setTextContent, + clearNode: clearNode, +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/vendor/assets/javascripts/xterm/encoding-indexes.js b/vendor/assets/javascripts/xterm/encoding-indexes.js new file mode 100644 index 0000000000000000000000000000000000000000..5fd98f5577e7c12923115a1c6663c95f367a6551 --- /dev/null +++ b/vendor/assets/javascripts/xterm/encoding-indexes.js @@ -0,0 +1,39 @@ +(function(global) { + 'use strict'; + global["encoding-indexes"] = +{ + "big5":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,17392,19506,17923,17830,17784,160359,19831,17843,162993,19682,163013,15253,18230,18244,19527,19520,148159,144919,160594,159371,159954,19543,172881,18255,17882,19589,162924,19719,19108,18081,158499,29221,154196,137827,146950,147297,26189,22267,null,32149,22813,166841,15860,38708,162799,23515,138590,23204,13861,171696,23249,23479,23804,26478,34195,170309,29793,29853,14453,138579,145054,155681,16108,153822,15093,31484,40855,147809,166157,143850,133770,143966,17162,33924,40854,37935,18736,34323,22678,38730,37400,31184,31282,26208,27177,34973,29772,31685,26498,31276,21071,36934,13542,29636,155065,29894,40903,22451,18735,21580,16689,145038,22552,31346,162661,35727,18094,159368,16769,155033,31662,140476,40904,140481,140489,140492,40905,34052,144827,16564,40906,17633,175615,25281,28782,40907,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,12736,12737,12738,12739,12740,131340,12741,131281,131277,12742,12743,131275,139240,12744,131274,12745,12746,12747,12748,131342,12749,12750,256,193,461,192,274,201,282,200,332,211,465,210,null,7870,null,7872,202,257,225,462,224,593,275,233,283,232,299,237,464,236,333,243,466,242,363,250,468,249,470,472,474,476,252,null,7871,null,7873,234,609,9178,9179,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,172969,135493,null,25866,null,null,20029,28381,40270,37343,null,null,161589,25745,20250,20264,20392,20822,20852,20892,20964,21153,21160,21307,21326,21457,21464,22242,22768,22788,22791,22834,22836,23398,23454,23455,23706,24198,24635,25993,26622,26628,26725,27982,28860,30005,32420,32428,32442,32455,32463,32479,32518,32567,33402,33487,33647,35270,35774,35810,36710,36711,36718,29713,31996,32205,26950,31433,21031,null,null,null,null,37260,30904,37214,32956,null,36107,33014,133607,null,null,32927,40647,19661,40393,40460,19518,171510,159758,40458,172339,13761,null,28314,33342,29977,null,18705,39532,39567,40857,31111,164972,138698,132560,142054,20004,20097,20096,20103,20159,20203,20279,13388,20413,15944,20483,20616,13437,13459,13477,20870,22789,20955,20988,20997,20105,21113,21136,21287,13767,21417,13649,21424,13651,21442,21539,13677,13682,13953,21651,21667,21684,21689,21712,21743,21784,21795,21800,13720,21823,13733,13759,21975,13765,163204,21797,null,134210,134421,151851,21904,142534,14828,131905,36422,150968,169189,16467,164030,30586,142392,14900,18389,164189,158194,151018,25821,134524,135092,134357,135412,25741,36478,134806,134155,135012,142505,164438,148691,null,134470,170573,164073,18420,151207,142530,39602,14951,169460,16365,13574,152263,169940,161992,142660,40302,38933,null,17369,155813,25780,21731,142668,142282,135287,14843,135279,157402,157462,162208,25834,151634,134211,36456,139681,166732,132913,null,18443,131497,16378,22643,142733,null,148936,132348,155799,134988,134550,21881,16571,17338,null,19124,141926,135325,33194,39157,134556,25465,14846,141173,36288,22177,25724,15939,null,173569,134665,142031,142537,null,135368,145858,14738,14854,164507,13688,155209,139463,22098,134961,142514,169760,13500,27709,151099,null,null,161140,142987,139784,173659,167117,134778,134196,157724,32659,135375,141315,141625,13819,152035,134796,135053,134826,16275,134960,134471,135503,134732,null,134827,134057,134472,135360,135485,16377,140950,25650,135085,144372,161337,142286,134526,134527,142417,142421,14872,134808,135367,134958,173618,158544,167122,167321,167114,38314,21708,33476,21945,null,171715,39974,39606,161630,142830,28992,33133,33004,23580,157042,33076,14231,21343,164029,37302,134906,134671,134775,134907,13789,151019,13833,134358,22191,141237,135369,134672,134776,135288,135496,164359,136277,134777,151120,142756,23124,135197,135198,135413,135414,22428,134673,161428,164557,135093,134779,151934,14083,135094,135552,152280,172733,149978,137274,147831,164476,22681,21096,13850,153405,31666,23400,18432,19244,40743,18919,39967,39821,154484,143677,22011,13810,22153,20008,22786,138177,194680,38737,131206,20059,20155,13630,23587,24401,24516,14586,25164,25909,27514,27701,27706,28780,29227,20012,29357,149737,32594,31035,31993,32595,156266,13505,null,156491,32770,32896,157202,158033,21341,34916,35265,161970,35744,36125,38021,38264,38271,38376,167439,38886,39029,39118,39134,39267,170000,40060,40479,40644,27503,63751,20023,131207,38429,25143,38050,null,20539,28158,171123,40870,15817,34959,147790,28791,23797,19232,152013,13657,154928,24866,166450,36775,37366,29073,26393,29626,144001,172295,15499,137600,19216,30948,29698,20910,165647,16393,27235,172730,16931,34319,133743,31274,170311,166634,38741,28749,21284,139390,37876,30425,166371,40871,30685,20131,20464,20668,20015,20247,40872,21556,32139,22674,22736,138678,24210,24217,24514,141074,25995,144377,26905,27203,146531,27903,null,29184,148741,29580,16091,150035,23317,29881,35715,154788,153237,31379,31724,31939,32364,33528,34199,40873,34960,40874,36537,40875,36815,34143,39392,37409,40876,167353,136255,16497,17058,23066,null,null,null,39016,26475,17014,22333,null,34262,149883,33471,160013,19585,159092,23931,158485,159678,40877,40878,23446,40879,26343,32347,28247,31178,15752,17603,143958,141206,17306,17718,null,23765,146202,35577,23672,15634,144721,23928,40882,29015,17752,147692,138787,19575,14712,13386,131492,158785,35532,20404,131641,22975,33132,38998,170234,24379,134047,null,139713,166253,16642,18107,168057,16135,40883,172469,16632,14294,18167,158790,16764,165554,160767,17773,14548,152730,17761,17691,19849,19579,19830,17898,16328,150287,13921,17630,17597,16877,23870,23880,23894,15868,14351,23972,23993,14368,14392,24130,24253,24357,24451,14600,14612,14655,14669,24791,24893,23781,14729,25015,25017,25039,14776,25132,25232,25317,25368,14840,22193,14851,25570,25595,25607,25690,14923,25792,23829,22049,40863,14999,25990,15037,26111,26195,15090,26258,15138,26390,15170,26532,26624,15192,26698,26756,15218,15217,15227,26889,26947,29276,26980,27039,27013,15292,27094,15325,27237,27252,27249,27266,15340,27289,15346,27307,27317,27348,27382,27521,27585,27626,27765,27818,15563,27906,27910,27942,28033,15599,28068,28081,28181,28184,28201,28294,166336,28347,28386,28378,40831,28392,28393,28452,28468,15686,147265,28545,28606,15722,15733,29111,23705,15754,28716,15761,28752,28756,28783,28799,28809,131877,17345,13809,134872,147159,22462,159443,28990,153568,13902,27042,166889,23412,31305,153825,169177,31333,31357,154028,31419,31408,31426,31427,29137,156813,16842,31450,31453,31466,16879,21682,154625,31499,31573,31529,152334,154878,31650,31599,33692,154548,158847,31696,33825,31634,31672,154912,15789,154725,33938,31738,31750,31797,154817,31812,31875,149634,31910,26237,148856,31945,31943,31974,31860,31987,31989,31950,32359,17693,159300,32093,159446,29837,32137,32171,28981,32179,32210,147543,155689,32228,15635,32245,137209,32229,164717,32285,155937,155994,32366,32402,17195,37996,32295,32576,32577,32583,31030,156368,39393,32663,156497,32675,136801,131176,17756,145254,17667,164666,32762,156809,32773,32776,32797,32808,32815,172167,158915,32827,32828,32865,141076,18825,157222,146915,157416,26405,32935,166472,33031,33050,22704,141046,27775,156824,151480,25831,136330,33304,137310,27219,150117,150165,17530,33321,133901,158290,146814,20473,136445,34018,33634,158474,149927,144688,137075,146936,33450,26907,194964,16859,34123,33488,33562,134678,137140,14017,143741,144730,33403,33506,33560,147083,159139,158469,158615,144846,15807,33565,21996,33669,17675,159141,33708,33729,33747,13438,159444,27223,34138,13462,159298,143087,33880,154596,33905,15827,17636,27303,33866,146613,31064,33960,158614,159351,159299,34014,33807,33681,17568,33939,34020,154769,16960,154816,17731,34100,23282,159385,17703,34163,17686,26559,34326,165413,165435,34241,159880,34306,136578,159949,194994,17770,34344,13896,137378,21495,160666,34430,34673,172280,34798,142375,34737,34778,34831,22113,34412,26710,17935,34885,34886,161248,146873,161252,34910,34972,18011,34996,34997,25537,35013,30583,161551,35207,35210,35238,35241,35239,35260,166437,35303,162084,162493,35484,30611,37374,35472,162393,31465,162618,147343,18195,162616,29052,35596,35615,152624,152933,35647,35660,35661,35497,150138,35728,35739,35503,136927,17941,34895,35995,163156,163215,195028,14117,163155,36054,163224,163261,36114,36099,137488,36059,28764,36113,150729,16080,36215,36265,163842,135188,149898,15228,164284,160012,31463,36525,36534,36547,37588,36633,36653,164709,164882,36773,37635,172703,133712,36787,18730,166366,165181,146875,24312,143970,36857,172052,165564,165121,140069,14720,159447,36919,165180,162494,36961,165228,165387,37032,165651,37060,165606,37038,37117,37223,15088,37289,37316,31916,166195,138889,37390,27807,37441,37474,153017,37561,166598,146587,166668,153051,134449,37676,37739,166625,166891,28815,23235,166626,166629,18789,37444,166892,166969,166911,37747,37979,36540,38277,38310,37926,38304,28662,17081,140922,165592,135804,146990,18911,27676,38523,38550,16748,38563,159445,25050,38582,30965,166624,38589,21452,18849,158904,131700,156688,168111,168165,150225,137493,144138,38705,34370,38710,18959,17725,17797,150249,28789,23361,38683,38748,168405,38743,23370,168427,38751,37925,20688,143543,143548,38793,38815,38833,38846,38848,38866,38880,152684,38894,29724,169011,38911,38901,168989,162170,19153,38964,38963,38987,39014,15118,160117,15697,132656,147804,153350,39114,39095,39112,39111,19199,159015,136915,21936,39137,39142,39148,37752,39225,150057,19314,170071,170245,39413,39436,39483,39440,39512,153381,14020,168113,170965,39648,39650,170757,39668,19470,39700,39725,165376,20532,39732,158120,14531,143485,39760,39744,171326,23109,137315,39822,148043,39938,39935,39948,171624,40404,171959,172434,172459,172257,172323,172511,40318,40323,172340,40462,26760,40388,139611,172435,172576,137531,172595,40249,172217,172724,40592,40597,40606,40610,19764,40618,40623,148324,40641,15200,14821,15645,20274,14270,166955,40706,40712,19350,37924,159138,40727,40726,40761,22175,22154,40773,39352,168075,38898,33919,40802,40809,31452,40846,29206,19390,149877,149947,29047,150008,148296,150097,29598,166874,137466,31135,166270,167478,37737,37875,166468,37612,37761,37835,166252,148665,29207,16107,30578,31299,28880,148595,148472,29054,137199,28835,137406,144793,16071,137349,152623,137208,14114,136955,137273,14049,137076,137425,155467,14115,136896,22363,150053,136190,135848,136134,136374,34051,145062,34051,33877,149908,160101,146993,152924,147195,159826,17652,145134,170397,159526,26617,14131,15381,15847,22636,137506,26640,16471,145215,147681,147595,147727,158753,21707,22174,157361,22162,135135,134056,134669,37830,166675,37788,20216,20779,14361,148534,20156,132197,131967,20299,20362,153169,23144,131499,132043,14745,131850,132116,13365,20265,131776,167603,131701,35546,131596,20120,20685,20749,20386,20227,150030,147082,20290,20526,20588,20609,20428,20453,20568,20732,20825,20827,20829,20830,28278,144789,147001,147135,28018,137348,147081,20904,20931,132576,17629,132259,132242,132241,36218,166556,132878,21081,21156,133235,21217,37742,18042,29068,148364,134176,149932,135396,27089,134685,29817,16094,29849,29716,29782,29592,19342,150204,147597,21456,13700,29199,147657,21940,131909,21709,134086,22301,37469,38644,37734,22493,22413,22399,13886,22731,23193,166470,136954,137071,136976,23084,22968,37519,23166,23247,23058,153926,137715,137313,148117,14069,27909,29763,23073,155267,23169,166871,132115,37856,29836,135939,28933,18802,37896,166395,37821,14240,23582,23710,24158,24136,137622,137596,146158,24269,23375,137475,137476,14081,137376,14045,136958,14035,33066,166471,138682,144498,166312,24332,24334,137511,137131,23147,137019,23364,34324,161277,34912,24702,141408,140843,24539,16056,140719,140734,168072,159603,25024,131134,131142,140827,24985,24984,24693,142491,142599,149204,168269,25713,149093,142186,14889,142114,144464,170218,142968,25399,173147,25782,25393,25553,149987,142695,25252,142497,25659,25963,26994,15348,143502,144045,149897,144043,21773,144096,137433,169023,26318,144009,143795,15072,16784,152964,166690,152975,136956,152923,152613,30958,143619,137258,143924,13412,143887,143746,148169,26254,159012,26219,19347,26160,161904,138731,26211,144082,144097,26142,153714,14545,145466,145340,15257,145314,144382,29904,15254,26511,149034,26806,26654,15300,27326,14435,145365,148615,27187,27218,27337,27397,137490,25873,26776,27212,15319,27258,27479,147392,146586,37792,37618,166890,166603,37513,163870,166364,37991,28069,28427,149996,28007,147327,15759,28164,147516,23101,28170,22599,27940,30786,28987,148250,148086,28913,29264,29319,29332,149391,149285,20857,150180,132587,29818,147192,144991,150090,149783,155617,16134,16049,150239,166947,147253,24743,16115,29900,29756,37767,29751,17567,159210,17745,30083,16227,150745,150790,16216,30037,30323,173510,15129,29800,166604,149931,149902,15099,15821,150094,16127,149957,149747,37370,22322,37698,166627,137316,20703,152097,152039,30584,143922,30478,30479,30587,149143,145281,14942,149744,29752,29851,16063,150202,150215,16584,150166,156078,37639,152961,30750,30861,30856,30930,29648,31065,161601,153315,16654,31131,33942,31141,27181,147194,31290,31220,16750,136934,16690,37429,31217,134476,149900,131737,146874,137070,13719,21867,13680,13994,131540,134157,31458,23129,141045,154287,154268,23053,131675,30960,23082,154566,31486,16889,31837,31853,16913,154547,155324,155302,31949,150009,137136,31886,31868,31918,27314,32220,32263,32211,32590,156257,155996,162632,32151,155266,17002,158581,133398,26582,131150,144847,22468,156690,156664,149858,32733,31527,133164,154345,154947,31500,155150,39398,34373,39523,27164,144447,14818,150007,157101,39455,157088,33920,160039,158929,17642,33079,17410,32966,33033,33090,157620,39107,158274,33378,33381,158289,33875,159143,34320,160283,23174,16767,137280,23339,137377,23268,137432,34464,195004,146831,34861,160802,23042,34926,20293,34951,35007,35046,35173,35149,153219,35156,161669,161668,166901,166873,166812,166393,16045,33955,18165,18127,14322,35389,35356,169032,24397,37419,148100,26068,28969,28868,137285,40301,35999,36073,163292,22938,30659,23024,17262,14036,36394,36519,150537,36656,36682,17140,27736,28603,140065,18587,28537,28299,137178,39913,14005,149807,37051,37015,21873,18694,37307,37892,166475,16482,166652,37927,166941,166971,34021,35371,38297,38311,38295,38294,167220,29765,16066,149759,150082,148458,16103,143909,38543,167655,167526,167525,16076,149997,150136,147438,29714,29803,16124,38721,168112,26695,18973,168083,153567,38749,37736,166281,166950,166703,156606,37562,23313,35689,18748,29689,147995,38811,38769,39224,134950,24001,166853,150194,38943,169178,37622,169431,37349,17600,166736,150119,166756,39132,166469,16128,37418,18725,33812,39227,39245,162566,15869,39323,19311,39338,39516,166757,153800,27279,39457,23294,39471,170225,19344,170312,39356,19389,19351,37757,22642,135938,22562,149944,136424,30788,141087,146872,26821,15741,37976,14631,24912,141185,141675,24839,40015,40019,40059,39989,39952,39807,39887,171565,39839,172533,172286,40225,19630,147716,40472,19632,40204,172468,172269,172275,170287,40357,33981,159250,159711,158594,34300,17715,159140,159364,159216,33824,34286,159232,145367,155748,31202,144796,144960,18733,149982,15714,37851,37566,37704,131775,30905,37495,37965,20452,13376,36964,152925,30781,30804,30902,30795,137047,143817,149825,13978,20338,28634,28633,28702,28702,21524,147893,22459,22771,22410,40214,22487,28980,13487,147884,29163,158784,151447,23336,137141,166473,24844,23246,23051,17084,148616,14124,19323,166396,37819,37816,137430,134941,33906,158912,136211,148218,142374,148417,22932,146871,157505,32168,155995,155812,149945,149899,166394,37605,29666,16105,29876,166755,137375,16097,150195,27352,29683,29691,16086,150078,150164,137177,150118,132007,136228,149989,29768,149782,28837,149878,37508,29670,37727,132350,37681,166606,166422,37766,166887,153045,18741,166530,29035,149827,134399,22180,132634,134123,134328,21762,31172,137210,32254,136898,150096,137298,17710,37889,14090,166592,149933,22960,137407,137347,160900,23201,14050,146779,14000,37471,23161,166529,137314,37748,15565,133812,19094,14730,20724,15721,15692,136092,29045,17147,164376,28175,168164,17643,27991,163407,28775,27823,15574,147437,146989,28162,28428,15727,132085,30033,14012,13512,18048,16090,18545,22980,37486,18750,36673,166940,158656,22546,22472,14038,136274,28926,148322,150129,143331,135856,140221,26809,26983,136088,144613,162804,145119,166531,145366,144378,150687,27162,145069,158903,33854,17631,17614,159014,159057,158850,159710,28439,160009,33597,137018,33773,158848,159827,137179,22921,23170,137139,23137,23153,137477,147964,14125,23023,137020,14023,29070,37776,26266,148133,23150,23083,148115,27179,147193,161590,148571,148170,28957,148057,166369,20400,159016,23746,148686,163405,148413,27148,148054,135940,28838,28979,148457,15781,27871,194597,150095,32357,23019,23855,15859,24412,150109,137183,32164,33830,21637,146170,144128,131604,22398,133333,132633,16357,139166,172726,28675,168283,23920,29583,31955,166489,168992,20424,32743,29389,29456,162548,29496,29497,153334,29505,29512,16041,162584,36972,29173,149746,29665,33270,16074,30476,16081,27810,22269,29721,29726,29727,16098,16112,16116,16122,29907,16142,16211,30018,30061,30066,30093,16252,30152,30172,16320,30285,16343,30324,16348,30330,151388,29064,22051,35200,22633,16413,30531,16441,26465,16453,13787,30616,16490,16495,23646,30654,30667,22770,30744,28857,30748,16552,30777,30791,30801,30822,33864,152885,31027,26627,31026,16643,16649,31121,31129,36795,31238,36796,16743,31377,16818,31420,33401,16836,31439,31451,16847,20001,31586,31596,31611,31762,31771,16992,17018,31867,31900,17036,31928,17044,31981,36755,28864,134351,32207,32212,32208,32253,32686,32692,29343,17303,32800,32805,31545,32814,32817,32852,15820,22452,28832,32951,33001,17389,33036,29482,33038,33042,30048,33044,17409,15161,33110,33113,33114,17427,22586,33148,33156,17445,33171,17453,33189,22511,33217,33252,33364,17551,33446,33398,33482,33496,33535,17584,33623,38505,27018,33797,28917,33892,24803,33928,17668,33982,34017,34040,34064,34104,34130,17723,34159,34160,34272,17783,34418,34450,34482,34543,38469,34699,17926,17943,34990,35071,35108,35143,35217,162151,35369,35384,35476,35508,35921,36052,36082,36124,18328,22623,36291,18413,20206,36410,21976,22356,36465,22005,36528,18487,36558,36578,36580,36589,36594,36791,36801,36810,36812,36915,39364,18605,39136,37395,18718,37416,37464,37483,37553,37550,37567,37603,37611,37619,37620,37629,37699,37764,37805,18757,18769,40639,37911,21249,37917,37933,37950,18794,37972,38009,38189,38306,18855,38388,38451,18917,26528,18980,38720,18997,38834,38850,22100,19172,24808,39097,19225,39153,22596,39182,39193,20916,39196,39223,39234,39261,39266,19312,39365,19357,39484,39695,31363,39785,39809,39901,39921,39924,19565,39968,14191,138178,40265,39994,40702,22096,40339,40381,40384,40444,38134,36790,40571,40620,40625,40637,40646,38108,40674,40689,40696,31432,40772,131220,131767,132000,26906,38083,22956,132311,22592,38081,14265,132565,132629,132726,136890,22359,29043,133826,133837,134079,21610,194619,134091,21662,134139,134203,134227,134245,134268,24807,134285,22138,134325,134365,134381,134511,134578,134600,26965,39983,34725,134660,134670,134871,135056,134957,134771,23584,135100,24075,135260,135247,135286,26398,135291,135304,135318,13895,135359,135379,135471,135483,21348,33965,135907,136053,135990,35713,136567,136729,137155,137159,20088,28859,137261,137578,137773,137797,138282,138352,138412,138952,25283,138965,139029,29080,26709,139333,27113,14024,139900,140247,140282,141098,141425,141647,33533,141671,141715,142037,35237,142056,36768,142094,38840,142143,38983,39613,142412,null,142472,142519,154600,142600,142610,142775,142741,142914,143220,143308,143411,143462,144159,144350,24497,26184,26303,162425,144743,144883,29185,149946,30679,144922,145174,32391,131910,22709,26382,26904,146087,161367,155618,146961,147129,161278,139418,18640,19128,147737,166554,148206,148237,147515,148276,148374,150085,132554,20946,132625,22943,138920,15294,146687,148484,148694,22408,149108,14747,149295,165352,170441,14178,139715,35678,166734,39382,149522,149755,150037,29193,150208,134264,22885,151205,151430,132985,36570,151596,21135,22335,29041,152217,152601,147274,150183,21948,152646,152686,158546,37332,13427,152895,161330,152926,18200,152930,152934,153543,149823,153693,20582,13563,144332,24798,153859,18300,166216,154286,154505,154630,138640,22433,29009,28598,155906,162834,36950,156082,151450,35682,156674,156746,23899,158711,36662,156804,137500,35562,150006,156808,147439,156946,19392,157119,157365,141083,37989,153569,24981,23079,194765,20411,22201,148769,157436,20074,149812,38486,28047,158909,13848,35191,157593,157806,156689,157790,29151,157895,31554,168128,133649,157990,37124,158009,31301,40432,158202,39462,158253,13919,156777,131105,31107,158260,158555,23852,144665,33743,158621,18128,158884,30011,34917,159150,22710,14108,140685,159819,160205,15444,160384,160389,37505,139642,160395,37680,160486,149968,27705,38047,160848,134904,34855,35061,141606,164979,137137,28344,150058,137248,14756,14009,23568,31203,17727,26294,171181,170148,35139,161740,161880,22230,16607,136714,14753,145199,164072,136133,29101,33638,162269,168360,23143,19639,159919,166315,162301,162314,162571,163174,147834,31555,31102,163849,28597,172767,27139,164632,21410,159239,37823,26678,38749,164207,163875,158133,136173,143919,163912,23941,166960,163971,22293,38947,166217,23979,149896,26046,27093,21458,150181,147329,15377,26422,163984,164084,164142,139169,164175,164233,164271,164378,164614,164655,164746,13770,164968,165546,18682,25574,166230,30728,37461,166328,17394,166375,17375,166376,166726,166868,23032,166921,36619,167877,168172,31569,168208,168252,15863,168286,150218,36816,29327,22155,169191,169449,169392,169400,169778,170193,170313,170346,170435,170536,170766,171354,171419,32415,171768,171811,19620,38215,172691,29090,172799,19857,36882,173515,19868,134300,36798,21953,36794,140464,36793,150163,17673,32383,28502,27313,20202,13540,166700,161949,14138,36480,137205,163876,166764,166809,162366,157359,15851,161365,146615,153141,153942,20122,155265,156248,22207,134765,36366,23405,147080,150686,25566,25296,137206,137339,25904,22061,154698,21530,152337,15814,171416,19581,22050,22046,32585,155352,22901,146752,34672,19996,135146,134473,145082,33047,40286,36120,30267,40005,30286,30649,37701,21554,33096,33527,22053,33074,33816,32957,21994,31074,22083,21526,134813,13774,22021,22001,26353,164578,13869,30004,22000,21946,21655,21874,134209,134294,24272,151880,134774,142434,134818,40619,32090,21982,135285,25245,38765,21652,36045,29174,37238,25596,25529,25598,21865,142147,40050,143027,20890,13535,134567,20903,21581,21790,21779,30310,36397,157834,30129,32950,34820,34694,35015,33206,33820,135361,17644,29444,149254,23440,33547,157843,22139,141044,163119,147875,163187,159440,160438,37232,135641,37384,146684,173737,134828,134905,29286,138402,18254,151490,163833,135147,16634,40029,25887,142752,18675,149472,171388,135148,134666,24674,161187,135149,null,155720,135559,29091,32398,40272,19994,19972,13687,23309,27826,21351,13996,14812,21373,13989,149016,22682,150382,33325,21579,22442,154261,133497,null,14930,140389,29556,171692,19721,39917,146686,171824,19547,151465,169374,171998,33884,146870,160434,157619,145184,25390,32037,147191,146988,14890,36872,21196,15988,13946,17897,132238,30272,23280,134838,30842,163630,22695,16575,22140,39819,23924,30292,173108,40581,19681,30201,14331,24857,143578,148466,null,22109,135849,22439,149859,171526,21044,159918,13741,27722,40316,31830,39737,22494,137068,23635,25811,169168,156469,160100,34477,134440,159010,150242,134513,null,20990,139023,23950,38659,138705,40577,36940,31519,39682,23761,31651,25192,25397,39679,31695,39722,31870,39726,31810,31878,39957,31740,39689,40727,39963,149822,40794,21875,23491,20477,40600,20466,21088,15878,21201,22375,20566,22967,24082,38856,40363,36700,21609,38836,39232,38842,21292,24880,26924,21466,39946,40194,19515,38465,27008,20646,30022,137069,39386,21107,null,37209,38529,37212,null,37201,167575,25471,159011,27338,22033,37262,30074,25221,132092,29519,31856,154657,146685,null,149785,30422,39837,20010,134356,33726,34882,null,23626,27072,20717,22394,21023,24053,20174,27697,131570,20281,21660,21722,21146,36226,13822,24332,13811,null,27474,37244,40869,39831,38958,39092,39610,40616,40580,29050,31508,null,27642,34840,32632,null,22048,173642,36471,40787,null,36308,36431,40476,36353,25218,164733,36392,36469,31443,150135,31294,30936,27882,35431,30215,166490,40742,27854,34774,30147,172722,30803,194624,36108,29410,29553,35629,29442,29937,36075,150203,34351,24506,34976,17591,null,137275,159237,null,35454,140571,null,24829,30311,39639,40260,37742,39823,34805,null,34831,36087,29484,38689,39856,13782,29362,19463,31825,39242,155993,24921,19460,40598,24957,null,22367,24943,25254,25145,25294,14940,25058,21418,144373,25444,26626,13778,23895,166850,36826,167481,null,20697,138566,30982,21298,38456,134971,16485,null,30718,null,31938,155418,31962,31277,32870,32867,32077,29957,29938,35220,33306,26380,32866,160902,32859,29936,33027,30500,35209,157644,30035,159441,34729,34766,33224,34700,35401,36013,35651,30507,29944,34010,13877,27058,36262,null,35241,29800,28089,34753,147473,29927,15835,29046,24740,24988,15569,29026,24695,null,32625,166701,29264,24809,19326,21024,15384,146631,155351,161366,152881,137540,135934,170243,159196,159917,23745,156077,166415,145015,131310,157766,151310,17762,23327,156492,40784,40614,156267,12288,65292,12289,12290,65294,8231,65307,65306,65311,65281,65072,8230,8229,65104,65105,65106,183,65108,65109,65110,65111,65372,8211,65073,8212,65075,9588,65076,65103,65288,65289,65077,65078,65371,65373,65079,65080,12308,12309,65081,65082,12304,12305,65083,65084,12298,12299,65085,65086,12296,12297,65087,65088,12300,12301,65089,65090,12302,12303,65091,65092,65113,65114,65115,65116,65117,65118,8216,8217,8220,8221,12317,12318,8245,8242,65283,65286,65290,8251,167,12291,9675,9679,9651,9650,9678,9734,9733,9671,9670,9633,9632,9661,9660,12963,8453,175,65507,65343,717,65097,65098,65101,65102,65099,65100,65119,65120,65121,65291,65293,215,247,177,8730,65308,65310,65309,8806,8807,8800,8734,8786,8801,65122,65123,65124,65125,65126,65374,8745,8746,8869,8736,8735,8895,13266,13265,8747,8750,8757,8756,9792,9794,8853,8857,8593,8595,8592,8594,8598,8599,8601,8600,8741,8739,65295,65340,8725,65128,65284,65509,12306,65504,65505,65285,65312,8451,8457,65129,65130,65131,13269,13212,13213,13214,13262,13217,13198,13199,13252,176,20825,20827,20830,20829,20833,20835,21991,29929,31950,9601,9602,9603,9604,9605,9606,9607,9608,9615,9614,9613,9612,9611,9610,9609,9532,9524,9516,9508,9500,9620,9472,9474,9621,9484,9488,9492,9496,9581,9582,9584,9583,9552,9566,9578,9569,9698,9699,9701,9700,9585,9586,9587,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,12321,12322,12323,12324,12325,12326,12327,12328,12329,21313,21316,21317,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,12549,12550,12551,12552,12553,12554,12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570,12571,12572,12573,12574,12575,12576,12577,12578,12579,12580,12581,12582,12583,12584,12585,729,713,714,711,715,9216,9217,9218,9219,9220,9221,9222,9223,9224,9225,9226,9227,9228,9229,9230,9231,9232,9233,9234,9235,9236,9237,9238,9239,9240,9241,9242,9243,9244,9245,9246,9247,9249,8364,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,19968,20057,19969,19971,20035,20061,20102,20108,20154,20799,20837,20843,20960,20992,20993,21147,21269,21313,21340,21448,19977,19979,19976,19978,20011,20024,20961,20037,20040,20063,20062,20110,20129,20800,20995,21242,21315,21449,21475,22303,22763,22805,22823,22899,23376,23377,23379,23544,23567,23586,23608,23665,24029,24037,24049,24050,24051,24062,24178,24318,24331,24339,25165,19985,19984,19981,20013,20016,20025,20043,23609,20104,20113,20117,20114,20116,20130,20161,20160,20163,20166,20167,20173,20170,20171,20164,20803,20801,20839,20845,20846,20844,20887,20982,20998,20999,21000,21243,21246,21247,21270,21305,21320,21319,21317,21342,21380,21451,21450,21453,22764,22825,22827,22826,22829,23380,23569,23588,23610,23663,24052,24187,24319,24340,24341,24515,25096,25142,25163,25166,25903,25991,26007,26020,26041,26085,26352,26376,26408,27424,27490,27513,27595,27604,27611,27663,27700,28779,29226,29238,29243,29255,29273,29275,29356,29579,19993,19990,19989,19988,19992,20027,20045,20047,20046,20197,20184,20180,20181,20182,20183,20195,20196,20185,20190,20805,20804,20873,20874,20908,20985,20986,20984,21002,21152,21151,21253,21254,21271,21277,20191,21322,21321,21345,21344,21359,21358,21435,21487,21476,21491,21484,21486,21481,21480,21500,21496,21493,21483,21478,21482,21490,21489,21488,21477,21485,21499,22235,22234,22806,22830,22833,22900,22902,23381,23427,23612,24040,24039,24038,24066,24067,24179,24188,24321,24344,24343,24517,25098,25171,25172,25170,25169,26021,26086,26414,26412,26410,26411,26413,27491,27597,27665,27664,27704,27713,27712,27710,29359,29572,29577,29916,29926,29976,29983,29992,29993,30000,30001,30002,30003,30091,30333,30382,30399,30446,30683,30690,30707,31034,31166,31348,31435,19998,19999,20050,20051,20073,20121,20132,20134,20133,20223,20233,20249,20234,20245,20237,20240,20241,20239,20210,20214,20219,20208,20211,20221,20225,20235,20809,20807,20806,20808,20840,20849,20877,20912,21015,21009,21010,21006,21014,21155,21256,21281,21280,21360,21361,21513,21519,21516,21514,21520,21505,21515,21508,21521,21517,21512,21507,21518,21510,21522,22240,22238,22237,22323,22320,22312,22317,22316,22319,22313,22809,22810,22839,22840,22916,22904,22915,22909,22905,22914,22913,23383,23384,23431,23432,23429,23433,23546,23574,23673,24030,24070,24182,24180,24335,24347,24537,24534,25102,25100,25101,25104,25187,25179,25176,25910,26089,26088,26092,26093,26354,26355,26377,26429,26420,26417,26421,27425,27492,27515,27670,27741,27735,27737,27743,27744,27728,27733,27745,27739,27725,27726,28784,29279,29277,30334,31481,31859,31992,32566,32650,32701,32769,32771,32780,32786,32819,32895,32905,32907,32908,33251,33258,33267,33276,33292,33307,33311,33390,33394,33406,34411,34880,34892,34915,35199,38433,20018,20136,20301,20303,20295,20311,20318,20276,20315,20309,20272,20304,20305,20285,20282,20280,20291,20308,20284,20294,20323,20316,20320,20271,20302,20278,20313,20317,20296,20314,20812,20811,20813,20853,20918,20919,21029,21028,21033,21034,21032,21163,21161,21162,21164,21283,21363,21365,21533,21549,21534,21566,21542,21582,21543,21574,21571,21555,21576,21570,21531,21545,21578,21561,21563,21560,21550,21557,21558,21536,21564,21568,21553,21547,21535,21548,22250,22256,22244,22251,22346,22353,22336,22349,22343,22350,22334,22352,22351,22331,22767,22846,22941,22930,22952,22942,22947,22937,22934,22925,22948,22931,22922,22949,23389,23388,23386,23387,23436,23435,23439,23596,23616,23617,23615,23614,23696,23697,23700,23692,24043,24076,24207,24199,24202,24311,24324,24351,24420,24418,24439,24441,24536,24524,24535,24525,24561,24555,24568,24554,25106,25105,25220,25239,25238,25216,25206,25225,25197,25226,25212,25214,25209,25203,25234,25199,25240,25198,25237,25235,25233,25222,25913,25915,25912,26097,26356,26463,26446,26447,26448,26449,26460,26454,26462,26441,26438,26464,26451,26455,27493,27599,27714,27742,27801,27777,27784,27785,27781,27803,27754,27770,27792,27760,27788,27752,27798,27794,27773,27779,27762,27774,27764,27782,27766,27789,27796,27800,27778,28790,28796,28797,28792,29282,29281,29280,29380,29378,29590,29996,29995,30007,30008,30338,30447,30691,31169,31168,31167,31350,31995,32597,32918,32915,32925,32920,32923,32922,32946,33391,33426,33419,33421,35211,35282,35328,35895,35910,35925,35997,36196,36208,36275,36523,36554,36763,36784,36802,36806,36805,36804,24033,37009,37026,37034,37030,37027,37193,37318,37324,38450,38446,38449,38442,38444,20006,20054,20083,20107,20123,20126,20139,20140,20335,20381,20365,20339,20351,20332,20379,20363,20358,20355,20336,20341,20360,20329,20347,20374,20350,20367,20369,20346,20820,20818,20821,20841,20855,20854,20856,20925,20989,21051,21048,21047,21050,21040,21038,21046,21057,21182,21179,21330,21332,21331,21329,21350,21367,21368,21369,21462,21460,21463,21619,21621,21654,21624,21653,21632,21627,21623,21636,21650,21638,21628,21648,21617,21622,21644,21658,21602,21608,21643,21629,21646,22266,22403,22391,22378,22377,22369,22374,22372,22396,22812,22857,22855,22856,22852,22868,22974,22971,22996,22969,22958,22993,22982,22992,22989,22987,22995,22986,22959,22963,22994,22981,23391,23396,23395,23447,23450,23448,23452,23449,23451,23578,23624,23621,23622,23735,23713,23736,23721,23723,23729,23731,24088,24090,24086,24085,24091,24081,24184,24218,24215,24220,24213,24214,24310,24358,24359,24361,24448,24449,24447,24444,24541,24544,24573,24565,24575,24591,24596,24623,24629,24598,24618,24597,24609,24615,24617,24619,24603,25110,25109,25151,25150,25152,25215,25289,25292,25284,25279,25282,25273,25298,25307,25259,25299,25300,25291,25288,25256,25277,25276,25296,25305,25287,25293,25269,25306,25265,25304,25302,25303,25286,25260,25294,25918,26023,26044,26106,26132,26131,26124,26118,26114,26126,26112,26127,26133,26122,26119,26381,26379,26477,26507,26517,26481,26524,26483,26487,26503,26525,26519,26479,26480,26495,26505,26494,26512,26485,26522,26515,26492,26474,26482,27427,27494,27495,27519,27667,27675,27875,27880,27891,27825,27852,27877,27827,27837,27838,27836,27874,27819,27861,27859,27832,27844,27833,27841,27822,27863,27845,27889,27839,27835,27873,27867,27850,27820,27887,27868,27862,27872,28821,28814,28818,28810,28825,29228,29229,29240,29256,29287,29289,29376,29390,29401,29399,29392,29609,29608,29599,29611,29605,30013,30109,30105,30106,30340,30402,30450,30452,30693,30717,31038,31040,31041,31177,31176,31354,31353,31482,31998,32596,32652,32651,32773,32954,32933,32930,32945,32929,32939,32937,32948,32938,32943,33253,33278,33293,33459,33437,33433,33453,33469,33439,33465,33457,33452,33445,33455,33464,33443,33456,33470,33463,34382,34417,21021,34920,36555,36814,36820,36817,37045,37048,37041,37046,37319,37329,38263,38272,38428,38464,38463,38459,38468,38466,38585,38632,38738,38750,20127,20141,20142,20449,20405,20399,20415,20448,20433,20431,20445,20419,20406,20440,20447,20426,20439,20398,20432,20420,20418,20442,20430,20446,20407,20823,20882,20881,20896,21070,21059,21066,21069,21068,21067,21063,21191,21193,21187,21185,21261,21335,21371,21402,21467,21676,21696,21672,21710,21705,21688,21670,21683,21703,21698,21693,21674,21697,21700,21704,21679,21675,21681,21691,21673,21671,21695,22271,22402,22411,22432,22435,22434,22478,22446,22419,22869,22865,22863,22862,22864,23004,23000,23039,23011,23016,23043,23013,23018,23002,23014,23041,23035,23401,23459,23462,23460,23458,23461,23553,23630,23631,23629,23627,23769,23762,24055,24093,24101,24095,24189,24224,24230,24314,24328,24365,24421,24456,24453,24458,24459,24455,24460,24457,24594,24605,24608,24613,24590,24616,24653,24688,24680,24674,24646,24643,24684,24683,24682,24676,25153,25308,25366,25353,25340,25325,25345,25326,25341,25351,25329,25335,25327,25324,25342,25332,25361,25346,25919,25925,26027,26045,26082,26149,26157,26144,26151,26159,26143,26152,26161,26148,26359,26623,26579,26609,26580,26576,26604,26550,26543,26613,26601,26607,26564,26577,26548,26586,26597,26552,26575,26590,26611,26544,26585,26594,26589,26578,27498,27523,27526,27573,27602,27607,27679,27849,27915,27954,27946,27969,27941,27916,27953,27934,27927,27963,27965,27966,27958,27931,27893,27961,27943,27960,27945,27950,27957,27918,27947,28843,28858,28851,28844,28847,28845,28856,28846,28836,29232,29298,29295,29300,29417,29408,29409,29623,29642,29627,29618,29645,29632,29619,29978,29997,30031,30028,30030,30027,30123,30116,30117,30114,30115,30328,30342,30343,30344,30408,30406,30403,30405,30465,30457,30456,30473,30475,30462,30460,30471,30684,30722,30740,30732,30733,31046,31049,31048,31047,31161,31162,31185,31186,31179,31359,31361,31487,31485,31869,32002,32005,32000,32009,32007,32004,32006,32568,32654,32703,32772,32784,32781,32785,32822,32982,32997,32986,32963,32964,32972,32993,32987,32974,32990,32996,32989,33268,33314,33511,33539,33541,33507,33499,33510,33540,33509,33538,33545,33490,33495,33521,33537,33500,33492,33489,33502,33491,33503,33519,33542,34384,34425,34427,34426,34893,34923,35201,35284,35336,35330,35331,35998,36000,36212,36211,36276,36557,36556,36848,36838,36834,36842,36837,36845,36843,36836,36840,37066,37070,37057,37059,37195,37194,37325,38274,38480,38475,38476,38477,38754,38761,38859,38893,38899,38913,39080,39131,39135,39318,39321,20056,20147,20492,20493,20515,20463,20518,20517,20472,20521,20502,20486,20540,20511,20506,20498,20497,20474,20480,20500,20520,20465,20513,20491,20505,20504,20467,20462,20525,20522,20478,20523,20489,20860,20900,20901,20898,20941,20940,20934,20939,21078,21084,21076,21083,21085,21290,21375,21407,21405,21471,21736,21776,21761,21815,21756,21733,21746,21766,21754,21780,21737,21741,21729,21769,21742,21738,21734,21799,21767,21757,21775,22275,22276,22466,22484,22475,22467,22537,22799,22871,22872,22874,23057,23064,23068,23071,23067,23059,23020,23072,23075,23081,23077,23052,23049,23403,23640,23472,23475,23478,23476,23470,23477,23481,23480,23556,23633,23637,23632,23789,23805,23803,23786,23784,23792,23798,23809,23796,24046,24109,24107,24235,24237,24231,24369,24466,24465,24464,24665,24675,24677,24656,24661,24685,24681,24687,24708,24735,24730,24717,24724,24716,24709,24726,25159,25331,25352,25343,25422,25406,25391,25429,25410,25414,25423,25417,25402,25424,25405,25386,25387,25384,25421,25420,25928,25929,26009,26049,26053,26178,26185,26191,26179,26194,26188,26181,26177,26360,26388,26389,26391,26657,26680,26696,26694,26707,26681,26690,26708,26665,26803,26647,26700,26705,26685,26612,26704,26688,26684,26691,26666,26693,26643,26648,26689,27530,27529,27575,27683,27687,27688,27686,27684,27888,28010,28053,28040,28039,28006,28024,28023,27993,28051,28012,28041,28014,27994,28020,28009,28044,28042,28025,28037,28005,28052,28874,28888,28900,28889,28872,28879,29241,29305,29436,29433,29437,29432,29431,29574,29677,29705,29678,29664,29674,29662,30036,30045,30044,30042,30041,30142,30149,30151,30130,30131,30141,30140,30137,30146,30136,30347,30384,30410,30413,30414,30505,30495,30496,30504,30697,30768,30759,30776,30749,30772,30775,30757,30765,30752,30751,30770,31061,31056,31072,31071,31062,31070,31069,31063,31066,31204,31203,31207,31199,31206,31209,31192,31364,31368,31449,31494,31505,31881,32033,32023,32011,32010,32032,32034,32020,32016,32021,32026,32028,32013,32025,32027,32570,32607,32660,32709,32705,32774,32792,32789,32793,32791,32829,32831,33009,33026,33008,33029,33005,33012,33030,33016,33011,33032,33021,33034,33020,33007,33261,33260,33280,33296,33322,33323,33320,33324,33467,33579,33618,33620,33610,33592,33616,33609,33589,33588,33615,33586,33593,33590,33559,33600,33585,33576,33603,34388,34442,34474,34451,34468,34473,34444,34467,34460,34928,34935,34945,34946,34941,34937,35352,35344,35342,35340,35349,35338,35351,35347,35350,35343,35345,35912,35962,35961,36001,36002,36215,36524,36562,36564,36559,36785,36865,36870,36855,36864,36858,36852,36867,36861,36869,36856,37013,37089,37085,37090,37202,37197,37196,37336,37341,37335,37340,37337,38275,38498,38499,38497,38491,38493,38500,38488,38494,38587,39138,39340,39592,39640,39717,39730,39740,20094,20602,20605,20572,20551,20547,20556,20570,20553,20581,20598,20558,20565,20597,20596,20599,20559,20495,20591,20589,20828,20885,20976,21098,21103,21202,21209,21208,21205,21264,21263,21273,21311,21312,21310,21443,26364,21830,21866,21862,21828,21854,21857,21827,21834,21809,21846,21839,21845,21807,21860,21816,21806,21852,21804,21859,21811,21825,21847,22280,22283,22281,22495,22533,22538,22534,22496,22500,22522,22530,22581,22519,22521,22816,22882,23094,23105,23113,23142,23146,23104,23100,23138,23130,23110,23114,23408,23495,23493,23492,23490,23487,23494,23561,23560,23559,23648,23644,23645,23815,23814,23822,23835,23830,23842,23825,23849,23828,23833,23844,23847,23831,24034,24120,24118,24115,24119,24247,24248,24246,24245,24254,24373,24375,24407,24428,24425,24427,24471,24473,24478,24472,24481,24480,24476,24703,24739,24713,24736,24744,24779,24756,24806,24765,24773,24763,24757,24796,24764,24792,24789,24774,24799,24760,24794,24775,25114,25115,25160,25504,25511,25458,25494,25506,25509,25463,25447,25496,25514,25457,25513,25481,25475,25499,25451,25512,25476,25480,25497,25505,25516,25490,25487,25472,25467,25449,25448,25466,25949,25942,25937,25945,25943,21855,25935,25944,25941,25940,26012,26011,26028,26063,26059,26060,26062,26205,26202,26212,26216,26214,26206,26361,21207,26395,26753,26799,26786,26771,26805,26751,26742,26801,26791,26775,26800,26755,26820,26797,26758,26757,26772,26781,26792,26783,26785,26754,27442,27578,27627,27628,27691,28046,28092,28147,28121,28082,28129,28108,28132,28155,28154,28165,28103,28107,28079,28113,28078,28126,28153,28088,28151,28149,28101,28114,28186,28085,28122,28139,28120,28138,28145,28142,28136,28102,28100,28074,28140,28095,28134,28921,28937,28938,28925,28911,29245,29309,29313,29468,29467,29462,29459,29465,29575,29701,29706,29699,29702,29694,29709,29920,29942,29943,29980,29986,30053,30054,30050,30064,30095,30164,30165,30133,30154,30157,30350,30420,30418,30427,30519,30526,30524,30518,30520,30522,30827,30787,30798,31077,31080,31085,31227,31378,31381,31520,31528,31515,31532,31526,31513,31518,31534,31890,31895,31893,32070,32067,32113,32046,32057,32060,32064,32048,32051,32068,32047,32066,32050,32049,32573,32670,32666,32716,32718,32722,32796,32842,32838,33071,33046,33059,33067,33065,33072,33060,33282,33333,33335,33334,33337,33678,33694,33688,33656,33698,33686,33725,33707,33682,33674,33683,33673,33696,33655,33659,33660,33670,33703,34389,24426,34503,34496,34486,34500,34485,34502,34507,34481,34479,34505,34899,34974,34952,34987,34962,34966,34957,34955,35219,35215,35370,35357,35363,35365,35377,35373,35359,35355,35362,35913,35930,36009,36012,36011,36008,36010,36007,36199,36198,36286,36282,36571,36575,36889,36877,36890,36887,36899,36895,36893,36880,36885,36894,36896,36879,36898,36886,36891,36884,37096,37101,37117,37207,37326,37365,37350,37347,37351,37357,37353,38281,38506,38517,38515,38520,38512,38516,38518,38519,38508,38592,38634,38633,31456,31455,38914,38915,39770,40165,40565,40575,40613,40635,20642,20621,20613,20633,20625,20608,20630,20632,20634,26368,20977,21106,21108,21109,21097,21214,21213,21211,21338,21413,21883,21888,21927,21884,21898,21917,21912,21890,21916,21930,21908,21895,21899,21891,21939,21934,21919,21822,21938,21914,21947,21932,21937,21886,21897,21931,21913,22285,22575,22570,22580,22564,22576,22577,22561,22557,22560,22777,22778,22880,23159,23194,23167,23186,23195,23207,23411,23409,23506,23500,23507,23504,23562,23563,23601,23884,23888,23860,23879,24061,24133,24125,24128,24131,24190,24266,24257,24258,24260,24380,24429,24489,24490,24488,24785,24801,24754,24758,24800,24860,24867,24826,24853,24816,24827,24820,24936,24817,24846,24822,24841,24832,24850,25119,25161,25507,25484,25551,25536,25577,25545,25542,25549,25554,25571,25552,25569,25558,25581,25582,25462,25588,25578,25563,25682,25562,25593,25950,25958,25954,25955,26001,26000,26031,26222,26224,26228,26230,26223,26257,26234,26238,26231,26366,26367,26399,26397,26874,26837,26848,26840,26839,26885,26847,26869,26862,26855,26873,26834,26866,26851,26827,26829,26893,26898,26894,26825,26842,26990,26875,27454,27450,27453,27544,27542,27580,27631,27694,27695,27692,28207,28216,28244,28193,28210,28263,28234,28192,28197,28195,28187,28251,28248,28196,28246,28270,28205,28198,28271,28212,28237,28218,28204,28227,28189,28222,28363,28297,28185,28238,28259,28228,28274,28265,28255,28953,28954,28966,28976,28961,28982,29038,28956,29260,29316,29312,29494,29477,29492,29481,29754,29738,29747,29730,29733,29749,29750,29748,29743,29723,29734,29736,29989,29990,30059,30058,30178,30171,30179,30169,30168,30174,30176,30331,30332,30358,30355,30388,30428,30543,30701,30813,30828,30831,31245,31240,31243,31237,31232,31384,31383,31382,31461,31459,31561,31574,31558,31568,31570,31572,31565,31563,31567,31569,31903,31909,32094,32080,32104,32085,32043,32110,32114,32097,32102,32098,32112,32115,21892,32724,32725,32779,32850,32901,33109,33108,33099,33105,33102,33081,33094,33086,33100,33107,33140,33298,33308,33769,33795,33784,33805,33760,33733,33803,33729,33775,33777,33780,33879,33802,33776,33804,33740,33789,33778,33738,33848,33806,33796,33756,33799,33748,33759,34395,34527,34521,34541,34516,34523,34532,34512,34526,34903,35009,35010,34993,35203,35222,35387,35424,35413,35422,35388,35393,35412,35419,35408,35398,35380,35386,35382,35414,35937,35970,36015,36028,36019,36029,36033,36027,36032,36020,36023,36022,36031,36024,36234,36229,36225,36302,36317,36299,36314,36305,36300,36315,36294,36603,36600,36604,36764,36910,36917,36913,36920,36914,36918,37122,37109,37129,37118,37219,37221,37327,37396,37397,37411,37385,37406,37389,37392,37383,37393,38292,38287,38283,38289,38291,38290,38286,38538,38542,38539,38525,38533,38534,38541,38514,38532,38593,38597,38596,38598,38599,38639,38642,38860,38917,38918,38920,39143,39146,39151,39145,39154,39149,39342,39341,40643,40653,40657,20098,20653,20661,20658,20659,20677,20670,20652,20663,20667,20655,20679,21119,21111,21117,21215,21222,21220,21218,21219,21295,21983,21992,21971,21990,21966,21980,21959,21969,21987,21988,21999,21978,21985,21957,21958,21989,21961,22290,22291,22622,22609,22616,22615,22618,22612,22635,22604,22637,22602,22626,22610,22603,22887,23233,23241,23244,23230,23229,23228,23219,23234,23218,23913,23919,24140,24185,24265,24264,24338,24409,24492,24494,24858,24847,24904,24863,24819,24859,24825,24833,24840,24910,24908,24900,24909,24894,24884,24871,24845,24838,24887,25121,25122,25619,25662,25630,25642,25645,25661,25644,25615,25628,25620,25613,25654,25622,25623,25606,25964,26015,26032,26263,26249,26247,26248,26262,26244,26264,26253,26371,27028,26989,26970,26999,26976,26964,26997,26928,27010,26954,26984,26987,26974,26963,27001,27014,26973,26979,26971,27463,27506,27584,27583,27603,27645,28322,28335,28371,28342,28354,28304,28317,28359,28357,28325,28312,28348,28346,28331,28369,28310,28316,28356,28372,28330,28327,28340,29006,29017,29033,29028,29001,29031,29020,29036,29030,29004,29029,29022,28998,29032,29014,29242,29266,29495,29509,29503,29502,29807,29786,29781,29791,29790,29761,29759,29785,29787,29788,30070,30072,30208,30192,30209,30194,30193,30202,30207,30196,30195,30430,30431,30555,30571,30566,30558,30563,30585,30570,30572,30556,30565,30568,30562,30702,30862,30896,30871,30872,30860,30857,30844,30865,30867,30847,31098,31103,31105,33836,31165,31260,31258,31264,31252,31263,31262,31391,31392,31607,31680,31584,31598,31591,31921,31923,31925,32147,32121,32145,32129,32143,32091,32622,32617,32618,32626,32681,32680,32676,32854,32856,32902,32900,33137,33136,33144,33125,33134,33139,33131,33145,33146,33126,33285,33351,33922,33911,33853,33841,33909,33894,33899,33865,33900,33883,33852,33845,33889,33891,33897,33901,33862,34398,34396,34399,34553,34579,34568,34567,34560,34558,34555,34562,34563,34566,34570,34905,35039,35028,35033,35036,35032,35037,35041,35018,35029,35026,35228,35299,35435,35442,35443,35430,35433,35440,35463,35452,35427,35488,35441,35461,35437,35426,35438,35436,35449,35451,35390,35432,35938,35978,35977,36042,36039,36040,36036,36018,36035,36034,36037,36321,36319,36328,36335,36339,36346,36330,36324,36326,36530,36611,36617,36606,36618,36767,36786,36939,36938,36947,36930,36948,36924,36949,36944,36935,36943,36942,36941,36945,36926,36929,37138,37143,37228,37226,37225,37321,37431,37463,37432,37437,37440,37438,37467,37451,37476,37457,37428,37449,37453,37445,37433,37439,37466,38296,38552,38548,38549,38605,38603,38601,38602,38647,38651,38649,38646,38742,38772,38774,38928,38929,38931,38922,38930,38924,39164,39156,39165,39166,39347,39345,39348,39649,40169,40578,40718,40723,40736,20711,20718,20709,20694,20717,20698,20693,20687,20689,20721,20686,20713,20834,20979,21123,21122,21297,21421,22014,22016,22043,22039,22013,22036,22022,22025,22029,22030,22007,22038,22047,22024,22032,22006,22296,22294,22645,22654,22659,22675,22666,22649,22661,22653,22781,22821,22818,22820,22890,22889,23265,23270,23273,23255,23254,23256,23267,23413,23518,23527,23521,23525,23526,23528,23522,23524,23519,23565,23650,23940,23943,24155,24163,24149,24151,24148,24275,24278,24330,24390,24432,24505,24903,24895,24907,24951,24930,24931,24927,24922,24920,24949,25130,25735,25688,25684,25764,25720,25695,25722,25681,25703,25652,25709,25723,25970,26017,26071,26070,26274,26280,26269,27036,27048,27029,27073,27054,27091,27083,27035,27063,27067,27051,27060,27088,27085,27053,27084,27046,27075,27043,27465,27468,27699,28467,28436,28414,28435,28404,28457,28478,28448,28460,28431,28418,28450,28415,28399,28422,28465,28472,28466,28451,28437,28459,28463,28552,28458,28396,28417,28402,28364,28407,29076,29081,29053,29066,29060,29074,29246,29330,29334,29508,29520,29796,29795,29802,29808,29805,29956,30097,30247,30221,30219,30217,30227,30433,30435,30596,30589,30591,30561,30913,30879,30887,30899,30889,30883,31118,31119,31117,31278,31281,31402,31401,31469,31471,31649,31637,31627,31605,31639,31645,31636,31631,31672,31623,31620,31929,31933,31934,32187,32176,32156,32189,32190,32160,32202,32180,32178,32177,32186,32162,32191,32181,32184,32173,32210,32199,32172,32624,32736,32737,32735,32862,32858,32903,33104,33152,33167,33160,33162,33151,33154,33255,33274,33287,33300,33310,33355,33993,33983,33990,33988,33945,33950,33970,33948,33995,33976,33984,34003,33936,33980,34001,33994,34623,34588,34619,34594,34597,34612,34584,34645,34615,34601,35059,35074,35060,35065,35064,35069,35048,35098,35055,35494,35468,35486,35491,35469,35489,35475,35492,35498,35493,35496,35480,35473,35482,35495,35946,35981,35980,36051,36049,36050,36203,36249,36245,36348,36628,36626,36629,36627,36771,36960,36952,36956,36963,36953,36958,36962,36957,36955,37145,37144,37150,37237,37240,37239,37236,37496,37504,37509,37528,37526,37499,37523,37532,37544,37500,37521,38305,38312,38313,38307,38309,38308,38553,38556,38555,38604,38610,38656,38780,38789,38902,38935,38936,39087,39089,39171,39173,39180,39177,39361,39599,39600,39654,39745,39746,40180,40182,40179,40636,40763,40778,20740,20736,20731,20725,20729,20738,20744,20745,20741,20956,21127,21128,21129,21133,21130,21232,21426,22062,22075,22073,22066,22079,22068,22057,22099,22094,22103,22132,22070,22063,22064,22656,22687,22686,22707,22684,22702,22697,22694,22893,23305,23291,23307,23285,23308,23304,23534,23532,23529,23531,23652,23653,23965,23956,24162,24159,24161,24290,24282,24287,24285,24291,24288,24392,24433,24503,24501,24950,24935,24942,24925,24917,24962,24956,24944,24939,24958,24999,24976,25003,24974,25004,24986,24996,24980,25006,25134,25705,25711,25721,25758,25778,25736,25744,25776,25765,25747,25749,25769,25746,25774,25773,25771,25754,25772,25753,25762,25779,25973,25975,25976,26286,26283,26292,26289,27171,27167,27112,27137,27166,27161,27133,27169,27155,27146,27123,27138,27141,27117,27153,27472,27470,27556,27589,27590,28479,28540,28548,28497,28518,28500,28550,28525,28507,28536,28526,28558,28538,28528,28516,28567,28504,28373,28527,28512,28511,29087,29100,29105,29096,29270,29339,29518,29527,29801,29835,29827,29822,29824,30079,30240,30249,30239,30244,30246,30241,30242,30362,30394,30436,30606,30599,30604,30609,30603,30923,30917,30906,30922,30910,30933,30908,30928,31295,31292,31296,31293,31287,31291,31407,31406,31661,31665,31684,31668,31686,31687,31681,31648,31692,31946,32224,32244,32239,32251,32216,32236,32221,32232,32227,32218,32222,32233,32158,32217,32242,32249,32629,32631,32687,32745,32806,33179,33180,33181,33184,33178,33176,34071,34109,34074,34030,34092,34093,34067,34065,34083,34081,34068,34028,34085,34047,34054,34690,34676,34678,34656,34662,34680,34664,34649,34647,34636,34643,34907,34909,35088,35079,35090,35091,35093,35082,35516,35538,35527,35524,35477,35531,35576,35506,35529,35522,35519,35504,35542,35533,35510,35513,35547,35916,35918,35948,36064,36062,36070,36068,36076,36077,36066,36067,36060,36074,36065,36205,36255,36259,36395,36368,36381,36386,36367,36393,36383,36385,36382,36538,36637,36635,36639,36649,36646,36650,36636,36638,36645,36969,36974,36968,36973,36983,37168,37165,37159,37169,37255,37257,37259,37251,37573,37563,37559,37610,37548,37604,37569,37555,37564,37586,37575,37616,37554,38317,38321,38660,38662,38663,38665,38752,38797,38795,38799,38945,38955,38940,39091,39178,39187,39186,39192,39389,39376,39391,39387,39377,39381,39378,39385,39607,39662,39663,39719,39749,39748,39799,39791,40198,40201,40195,40617,40638,40654,22696,40786,20754,20760,20756,20752,20757,20864,20906,20957,21137,21139,21235,22105,22123,22137,22121,22116,22136,22122,22120,22117,22129,22127,22124,22114,22134,22721,22718,22727,22725,22894,23325,23348,23416,23536,23566,24394,25010,24977,25001,24970,25037,25014,25022,25034,25032,25136,25797,25793,25803,25787,25788,25818,25796,25799,25794,25805,25791,25810,25812,25790,25972,26310,26313,26297,26308,26311,26296,27197,27192,27194,27225,27243,27224,27193,27204,27234,27233,27211,27207,27189,27231,27208,27481,27511,27653,28610,28593,28577,28611,28580,28609,28583,28595,28608,28601,28598,28582,28576,28596,29118,29129,29136,29138,29128,29141,29113,29134,29145,29148,29123,29124,29544,29852,29859,29848,29855,29854,29922,29964,29965,30260,30264,30266,30439,30437,30624,30622,30623,30629,30952,30938,30956,30951,31142,31309,31310,31302,31308,31307,31418,31705,31761,31689,31716,31707,31713,31721,31718,31957,31958,32266,32273,32264,32283,32291,32286,32285,32265,32272,32633,32690,32752,32753,32750,32808,33203,33193,33192,33275,33288,33368,33369,34122,34137,34120,34152,34153,34115,34121,34157,34154,34142,34691,34719,34718,34722,34701,34913,35114,35122,35109,35115,35105,35242,35238,35558,35578,35563,35569,35584,35548,35559,35566,35582,35585,35586,35575,35565,35571,35574,35580,35947,35949,35987,36084,36420,36401,36404,36418,36409,36405,36667,36655,36664,36659,36776,36774,36981,36980,36984,36978,36988,36986,37172,37266,37664,37686,37624,37683,37679,37666,37628,37675,37636,37658,37648,37670,37665,37653,37678,37657,38331,38567,38568,38570,38613,38670,38673,38678,38669,38675,38671,38747,38748,38758,38808,38960,38968,38971,38967,38957,38969,38948,39184,39208,39198,39195,39201,39194,39405,39394,39409,39608,39612,39675,39661,39720,39825,40213,40227,40230,40232,40210,40219,40664,40660,40845,40860,20778,20767,20769,20786,21237,22158,22144,22160,22149,22151,22159,22741,22739,22737,22734,23344,23338,23332,23418,23607,23656,23996,23994,23997,23992,24171,24396,24509,25033,25026,25031,25062,25035,25138,25140,25806,25802,25816,25824,25840,25830,25836,25841,25826,25837,25986,25987,26329,26326,27264,27284,27268,27298,27292,27355,27299,27262,27287,27280,27296,27484,27566,27610,27656,28632,28657,28639,28640,28635,28644,28651,28655,28544,28652,28641,28649,28629,28654,28656,29159,29151,29166,29158,29157,29165,29164,29172,29152,29237,29254,29552,29554,29865,29872,29862,29864,30278,30274,30284,30442,30643,30634,30640,30636,30631,30637,30703,30967,30970,30964,30959,30977,31143,31146,31319,31423,31751,31757,31742,31735,31756,31712,31968,31964,31966,31970,31967,31961,31965,32302,32318,32326,32311,32306,32323,32299,32317,32305,32325,32321,32308,32313,32328,32309,32319,32303,32580,32755,32764,32881,32882,32880,32879,32883,33222,33219,33210,33218,33216,33215,33213,33225,33214,33256,33289,33393,34218,34180,34174,34204,34193,34196,34223,34203,34183,34216,34186,34407,34752,34769,34739,34770,34758,34731,34747,34746,34760,34763,35131,35126,35140,35128,35133,35244,35598,35607,35609,35611,35594,35616,35613,35588,35600,35905,35903,35955,36090,36093,36092,36088,36091,36264,36425,36427,36424,36426,36676,36670,36674,36677,36671,36991,36989,36996,36993,36994,36992,37177,37283,37278,37276,37709,37762,37672,37749,37706,37733,37707,37656,37758,37740,37723,37744,37722,37716,38346,38347,38348,38344,38342,38577,38584,38614,38684,38686,38816,38867,38982,39094,39221,39425,39423,39854,39851,39850,39853,40251,40255,40587,40655,40670,40668,40669,40667,40766,40779,21474,22165,22190,22745,22744,23352,24413,25059,25139,25844,25842,25854,25862,25850,25851,25847,26039,26332,26406,27315,27308,27331,27323,27320,27330,27310,27311,27487,27512,27567,28681,28683,28670,28678,28666,28689,28687,29179,29180,29182,29176,29559,29557,29863,29887,29973,30294,30296,30290,30653,30655,30651,30652,30990,31150,31329,31330,31328,31428,31429,31787,31783,31786,31774,31779,31777,31975,32340,32341,32350,32346,32353,32338,32345,32584,32761,32763,32887,32886,33229,33231,33290,34255,34217,34253,34256,34249,34224,34234,34233,34214,34799,34796,34802,34784,35206,35250,35316,35624,35641,35628,35627,35920,36101,36441,36451,36454,36452,36447,36437,36544,36681,36685,36999,36995,37000,37291,37292,37328,37780,37770,37782,37794,37811,37806,37804,37808,37784,37786,37783,38356,38358,38352,38357,38626,38620,38617,38619,38622,38692,38819,38822,38829,38905,38989,38991,38988,38990,38995,39098,39230,39231,39229,39214,39333,39438,39617,39683,39686,39759,39758,39757,39882,39881,39933,39880,39872,40273,40285,40288,40672,40725,40748,20787,22181,22750,22751,22754,23541,40848,24300,25074,25079,25078,25077,25856,25871,26336,26333,27365,27357,27354,27347,28699,28703,28712,28698,28701,28693,28696,29190,29197,29272,29346,29560,29562,29885,29898,29923,30087,30086,30303,30305,30663,31001,31153,31339,31337,31806,31807,31800,31805,31799,31808,32363,32365,32377,32361,32362,32645,32371,32694,32697,32696,33240,34281,34269,34282,34261,34276,34277,34295,34811,34821,34829,34809,34814,35168,35167,35158,35166,35649,35676,35672,35657,35674,35662,35663,35654,35673,36104,36106,36476,36466,36487,36470,36460,36474,36468,36692,36686,36781,37002,37003,37297,37294,37857,37841,37855,37827,37832,37852,37853,37846,37858,37837,37848,37860,37847,37864,38364,38580,38627,38698,38695,38753,38876,38907,39006,39000,39003,39100,39237,39241,39446,39449,39693,39912,39911,39894,39899,40329,40289,40306,40298,40300,40594,40599,40595,40628,21240,22184,22199,22198,22196,22204,22756,23360,23363,23421,23542,24009,25080,25082,25880,25876,25881,26342,26407,27372,28734,28720,28722,29200,29563,29903,30306,30309,31014,31018,31020,31019,31431,31478,31820,31811,31821,31983,31984,36782,32381,32380,32386,32588,32768,33242,33382,34299,34297,34321,34298,34310,34315,34311,34314,34836,34837,35172,35258,35320,35696,35692,35686,35695,35679,35691,36111,36109,36489,36481,36485,36482,37300,37323,37912,37891,37885,38369,38704,39108,39250,39249,39336,39467,39472,39479,39477,39955,39949,40569,40629,40680,40751,40799,40803,40801,20791,20792,22209,22208,22210,22804,23660,24013,25084,25086,25885,25884,26005,26345,27387,27396,27386,27570,28748,29211,29351,29910,29908,30313,30675,31824,32399,32396,32700,34327,34349,34330,34851,34850,34849,34847,35178,35180,35261,35700,35703,35709,36115,36490,36493,36491,36703,36783,37306,37934,37939,37941,37946,37944,37938,37931,38370,38712,38713,38706,38911,39015,39013,39255,39493,39491,39488,39486,39631,39764,39761,39981,39973,40367,40372,40386,40376,40605,40687,40729,40796,40806,40807,20796,20795,22216,22218,22217,23423,24020,24018,24398,25087,25892,27402,27489,28753,28760,29568,29924,30090,30318,30316,31155,31840,31839,32894,32893,33247,35186,35183,35324,35712,36118,36119,36497,36499,36705,37192,37956,37969,37970,38717,38718,38851,38849,39019,39253,39509,39501,39634,39706,40009,39985,39998,39995,40403,40407,40756,40812,40810,40852,22220,24022,25088,25891,25899,25898,26348,27408,29914,31434,31844,31843,31845,32403,32406,32404,33250,34360,34367,34865,35722,37008,37007,37987,37984,37988,38760,39023,39260,39514,39515,39511,39635,39636,39633,40020,40023,40022,40421,40607,40692,22225,22761,25900,28766,30321,30322,30679,32592,32648,34870,34873,34914,35731,35730,35734,33399,36123,37312,37994,38722,38728,38724,38854,39024,39519,39714,39768,40031,40441,40442,40572,40573,40711,40823,40818,24307,27414,28771,31852,31854,34875,35264,36513,37313,38002,38000,39025,39262,39638,39715,40652,28772,30682,35738,38007,38857,39522,39525,32412,35740,36522,37317,38013,38014,38012,40055,40056,40695,35924,38015,40474,29224,39530,39729,40475,40478,31858,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,20022,20031,20101,20128,20866,20886,20907,21241,21304,21353,21430,22794,23424,24027,12083,24191,24308,24400,24417,25908,26080,30098,30326,36789,38582,168,710,12541,12542,12445,12446,12291,20189,12293,12294,12295,12540,65339,65341,10045,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,8679,8632,8633,12751,131276,20058,131210,20994,17553,40880,20872,40881,161287,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,65506,65508,65287,65282,12849,8470,8481,12443,12444,11904,11908,11910,11911,11912,11914,11916,11917,11925,11932,11933,11941,11943,11946,11948,11950,11958,11964,11966,11974,11978,11980,11981,11983,11990,11991,11998,12003,null,null,null,643,592,603,596,629,339,248,331,650,618,20034,20060,20981,21274,21378,19975,19980,20039,20109,22231,64012,23662,24435,19983,20871,19982,20014,20115,20162,20169,20168,20888,21244,21356,21433,22304,22787,22828,23568,24063,26081,27571,27596,27668,29247,20017,20028,20200,20188,20201,20193,20189,20186,21004,21276,21324,22306,22307,22807,22831,23425,23428,23570,23611,23668,23667,24068,24192,24194,24521,25097,25168,27669,27702,27715,27711,27707,29358,29360,29578,31160,32906,38430,20238,20248,20268,20213,20244,20209,20224,20215,20232,20253,20226,20229,20258,20243,20228,20212,20242,20913,21011,21001,21008,21158,21282,21279,21325,21386,21511,22241,22239,22318,22314,22324,22844,22912,22908,22917,22907,22910,22903,22911,23382,23573,23589,23676,23674,23675,23678,24031,24181,24196,24322,24346,24436,24533,24532,24527,25180,25182,25188,25185,25190,25186,25177,25184,25178,25189,26095,26094,26430,26425,26424,26427,26426,26431,26428,26419,27672,27718,27730,27740,27727,27722,27732,27723,27724,28785,29278,29364,29365,29582,29994,30335,31349,32593,33400,33404,33408,33405,33407,34381,35198,37017,37015,37016,37019,37012,38434,38436,38432,38435,20310,20283,20322,20297,20307,20324,20286,20327,20306,20319,20289,20312,20269,20275,20287,20321,20879,20921,21020,21022,21025,21165,21166,21257,21347,21362,21390,21391,21552,21559,21546,21588,21573,21529,21532,21541,21528,21565,21583,21569,21544,21540,21575,22254,22247,22245,22337,22341,22348,22345,22347,22354,22790,22848,22950,22936,22944,22935,22926,22946,22928,22927,22951,22945,23438,23442,23592,23594,23693,23695,23688,23691,23689,23698,23690,23686,23699,23701,24032,24074,24078,24203,24201,24204,24200,24205,24325,24349,24440,24438,24530,24529,24528,24557,24552,24558,24563,24545,24548,24547,24570,24559,24567,24571,24576,24564,25146,25219,25228,25230,25231,25236,25223,25201,25211,25210,25200,25217,25224,25207,25213,25202,25204,25911,26096,26100,26099,26098,26101,26437,26439,26457,26453,26444,26440,26461,26445,26458,26443,27600,27673,27674,27768,27751,27755,27780,27787,27791,27761,27759,27753,27802,27757,27783,27797,27804,27750,27763,27749,27771,27790,28788,28794,29283,29375,29373,29379,29382,29377,29370,29381,29589,29591,29587,29588,29586,30010,30009,30100,30101,30337,31037,32820,32917,32921,32912,32914,32924,33424,33423,33413,33422,33425,33427,33418,33411,33412,35960,36809,36799,37023,37025,37029,37022,37031,37024,38448,38440,38447,38445,20019,20376,20348,20357,20349,20352,20359,20342,20340,20361,20356,20343,20300,20375,20330,20378,20345,20353,20344,20368,20380,20372,20382,20370,20354,20373,20331,20334,20894,20924,20926,21045,21042,21043,21062,21041,21180,21258,21259,21308,21394,21396,21639,21631,21633,21649,21634,21640,21611,21626,21630,21605,21612,21620,21606,21645,21615,21601,21600,21656,21603,21607,21604,22263,22265,22383,22386,22381,22379,22385,22384,22390,22400,22389,22395,22387,22388,22370,22376,22397,22796,22853,22965,22970,22991,22990,22962,22988,22977,22966,22972,22979,22998,22961,22973,22976,22984,22964,22983,23394,23397,23443,23445,23620,23623,23726,23716,23712,23733,23727,23720,23724,23711,23715,23725,23714,23722,23719,23709,23717,23734,23728,23718,24087,24084,24089,24360,24354,24355,24356,24404,24450,24446,24445,24542,24549,24621,24614,24601,24626,24587,24628,24586,24599,24627,24602,24606,24620,24610,24589,24592,24622,24595,24593,24588,24585,24604,25108,25149,25261,25268,25297,25278,25258,25270,25290,25262,25267,25263,25275,25257,25264,25272,25917,26024,26043,26121,26108,26116,26130,26120,26107,26115,26123,26125,26117,26109,26129,26128,26358,26378,26501,26476,26510,26514,26486,26491,26520,26502,26500,26484,26509,26508,26490,26527,26513,26521,26499,26493,26497,26488,26489,26516,27429,27520,27518,27614,27677,27795,27884,27883,27886,27865,27830,27860,27821,27879,27831,27856,27842,27834,27843,27846,27885,27890,27858,27869,27828,27786,27805,27776,27870,27840,27952,27853,27847,27824,27897,27855,27881,27857,28820,28824,28805,28819,28806,28804,28817,28822,28802,28826,28803,29290,29398,29387,29400,29385,29404,29394,29396,29402,29388,29393,29604,29601,29613,29606,29602,29600,29612,29597,29917,29928,30015,30016,30014,30092,30104,30383,30451,30449,30448,30453,30712,30716,30713,30715,30714,30711,31042,31039,31173,31352,31355,31483,31861,31997,32821,32911,32942,32931,32952,32949,32941,33312,33440,33472,33451,33434,33432,33435,33461,33447,33454,33468,33438,33466,33460,33448,33441,33449,33474,33444,33475,33462,33442,34416,34415,34413,34414,35926,36818,36811,36819,36813,36822,36821,36823,37042,37044,37039,37043,37040,38457,38461,38460,38458,38467,20429,20421,20435,20402,20425,20427,20417,20436,20444,20441,20411,20403,20443,20423,20438,20410,20416,20409,20460,21060,21065,21184,21186,21309,21372,21399,21398,21401,21400,21690,21665,21677,21669,21711,21699,33549,21687,21678,21718,21686,21701,21702,21664,21616,21692,21666,21694,21618,21726,21680,22453,22430,22431,22436,22412,22423,22429,22427,22420,22424,22415,22425,22437,22426,22421,22772,22797,22867,23009,23006,23022,23040,23025,23005,23034,23037,23036,23030,23012,23026,23031,23003,23017,23027,23029,23008,23038,23028,23021,23464,23628,23760,23768,23756,23767,23755,23771,23774,23770,23753,23751,23754,23766,23763,23764,23759,23752,23750,23758,23775,23800,24057,24097,24098,24099,24096,24100,24240,24228,24226,24219,24227,24229,24327,24366,24406,24454,24631,24633,24660,24690,24670,24645,24659,24647,24649,24667,24652,24640,24642,24671,24612,24644,24664,24678,24686,25154,25155,25295,25357,25355,25333,25358,25347,25323,25337,25359,25356,25336,25334,25344,25363,25364,25338,25365,25339,25328,25921,25923,26026,26047,26166,26145,26162,26165,26140,26150,26146,26163,26155,26170,26141,26164,26169,26158,26383,26384,26561,26610,26568,26554,26588,26555,26616,26584,26560,26551,26565,26603,26596,26591,26549,26573,26547,26615,26614,26606,26595,26562,26553,26574,26599,26608,26546,26620,26566,26605,26572,26542,26598,26587,26618,26569,26570,26563,26602,26571,27432,27522,27524,27574,27606,27608,27616,27680,27681,27944,27956,27949,27935,27964,27967,27922,27914,27866,27955,27908,27929,27962,27930,27921,27904,27933,27970,27905,27928,27959,27907,27919,27968,27911,27936,27948,27912,27938,27913,27920,28855,28831,28862,28849,28848,28833,28852,28853,28841,29249,29257,29258,29292,29296,29299,29294,29386,29412,29416,29419,29407,29418,29414,29411,29573,29644,29634,29640,29637,29625,29622,29621,29620,29675,29631,29639,29630,29635,29638,29624,29643,29932,29934,29998,30023,30024,30119,30122,30329,30404,30472,30467,30468,30469,30474,30455,30459,30458,30695,30696,30726,30737,30738,30725,30736,30735,30734,30729,30723,30739,31050,31052,31051,31045,31044,31189,31181,31183,31190,31182,31360,31358,31441,31488,31489,31866,31864,31865,31871,31872,31873,32003,32008,32001,32600,32657,32653,32702,32775,32782,32783,32788,32823,32984,32967,32992,32977,32968,32962,32976,32965,32995,32985,32988,32970,32981,32969,32975,32983,32998,32973,33279,33313,33428,33497,33534,33529,33543,33512,33536,33493,33594,33515,33494,33524,33516,33505,33522,33525,33548,33531,33526,33520,33514,33508,33504,33530,33523,33517,34423,34420,34428,34419,34881,34894,34919,34922,34921,35283,35332,35335,36210,36835,36833,36846,36832,37105,37053,37055,37077,37061,37054,37063,37067,37064,37332,37331,38484,38479,38481,38483,38474,38478,20510,20485,20487,20499,20514,20528,20507,20469,20468,20531,20535,20524,20470,20471,20503,20508,20512,20519,20533,20527,20529,20494,20826,20884,20883,20938,20932,20933,20936,20942,21089,21082,21074,21086,21087,21077,21090,21197,21262,21406,21798,21730,21783,21778,21735,21747,21732,21786,21759,21764,21768,21739,21777,21765,21745,21770,21755,21751,21752,21728,21774,21763,21771,22273,22274,22476,22578,22485,22482,22458,22470,22461,22460,22456,22454,22463,22471,22480,22457,22465,22798,22858,23065,23062,23085,23086,23061,23055,23063,23050,23070,23091,23404,23463,23469,23468,23555,23638,23636,23788,23807,23790,23793,23799,23808,23801,24105,24104,24232,24238,24234,24236,24371,24368,24423,24669,24666,24679,24641,24738,24712,24704,24722,24705,24733,24707,24725,24731,24727,24711,24732,24718,25113,25158,25330,25360,25430,25388,25412,25413,25398,25411,25572,25401,25419,25418,25404,25385,25409,25396,25432,25428,25433,25389,25415,25395,25434,25425,25400,25431,25408,25416,25930,25926,26054,26051,26052,26050,26186,26207,26183,26193,26386,26387,26655,26650,26697,26674,26675,26683,26699,26703,26646,26673,26652,26677,26667,26669,26671,26702,26692,26676,26653,26642,26644,26662,26664,26670,26701,26682,26661,26656,27436,27439,27437,27441,27444,27501,32898,27528,27622,27620,27624,27619,27618,27623,27685,28026,28003,28004,28022,27917,28001,28050,27992,28002,28013,28015,28049,28045,28143,28031,28038,27998,28007,28000,28055,28016,28028,27999,28034,28056,27951,28008,28043,28030,28032,28036,27926,28035,28027,28029,28021,28048,28892,28883,28881,28893,28875,32569,28898,28887,28882,28894,28896,28884,28877,28869,28870,28871,28890,28878,28897,29250,29304,29303,29302,29440,29434,29428,29438,29430,29427,29435,29441,29651,29657,29669,29654,29628,29671,29667,29673,29660,29650,29659,29652,29661,29658,29655,29656,29672,29918,29919,29940,29941,29985,30043,30047,30128,30145,30139,30148,30144,30143,30134,30138,30346,30409,30493,30491,30480,30483,30482,30499,30481,30485,30489,30490,30498,30503,30755,30764,30754,30773,30767,30760,30766,30763,30753,30761,30771,30762,30769,31060,31067,31055,31068,31059,31058,31057,31211,31212,31200,31214,31213,31210,31196,31198,31197,31366,31369,31365,31371,31372,31370,31367,31448,31504,31492,31507,31493,31503,31496,31498,31502,31497,31506,31876,31889,31882,31884,31880,31885,31877,32030,32029,32017,32014,32024,32022,32019,32031,32018,32015,32012,32604,32609,32606,32608,32605,32603,32662,32658,32707,32706,32704,32790,32830,32825,33018,33010,33017,33013,33025,33019,33024,33281,33327,33317,33587,33581,33604,33561,33617,33573,33622,33599,33601,33574,33564,33570,33602,33614,33563,33578,33544,33596,33613,33558,33572,33568,33591,33583,33577,33607,33605,33612,33619,33566,33580,33611,33575,33608,34387,34386,34466,34472,34454,34445,34449,34462,34439,34455,34438,34443,34458,34437,34469,34457,34465,34471,34453,34456,34446,34461,34448,34452,34883,34884,34925,34933,34934,34930,34944,34929,34943,34927,34947,34942,34932,34940,35346,35911,35927,35963,36004,36003,36214,36216,36277,36279,36278,36561,36563,36862,36853,36866,36863,36859,36868,36860,36854,37078,37088,37081,37082,37091,37087,37093,37080,37083,37079,37084,37092,37200,37198,37199,37333,37346,37338,38492,38495,38588,39139,39647,39727,20095,20592,20586,20577,20574,20576,20563,20555,20573,20594,20552,20557,20545,20571,20554,20578,20501,20549,20575,20585,20587,20579,20580,20550,20544,20590,20595,20567,20561,20944,21099,21101,21100,21102,21206,21203,21293,21404,21877,21878,21820,21837,21840,21812,21802,21841,21858,21814,21813,21808,21842,21829,21772,21810,21861,21838,21817,21832,21805,21819,21824,21835,22282,22279,22523,22548,22498,22518,22492,22516,22528,22509,22525,22536,22520,22539,22515,22479,22535,22510,22499,22514,22501,22508,22497,22542,22524,22544,22503,22529,22540,22513,22505,22512,22541,22532,22876,23136,23128,23125,23143,23134,23096,23093,23149,23120,23135,23141,23148,23123,23140,23127,23107,23133,23122,23108,23131,23112,23182,23102,23117,23097,23116,23152,23145,23111,23121,23126,23106,23132,23410,23406,23489,23488,23641,23838,23819,23837,23834,23840,23820,23848,23821,23846,23845,23823,23856,23826,23843,23839,23854,24126,24116,24241,24244,24249,24242,24243,24374,24376,24475,24470,24479,24714,24720,24710,24766,24752,24762,24787,24788,24783,24804,24793,24797,24776,24753,24795,24759,24778,24767,24771,24781,24768,25394,25445,25482,25474,25469,25533,25502,25517,25501,25495,25515,25486,25455,25479,25488,25454,25519,25461,25500,25453,25518,25468,25508,25403,25503,25464,25477,25473,25489,25485,25456,25939,26061,26213,26209,26203,26201,26204,26210,26392,26745,26759,26768,26780,26733,26734,26798,26795,26966,26735,26787,26796,26793,26741,26740,26802,26767,26743,26770,26748,26731,26738,26794,26752,26737,26750,26779,26774,26763,26784,26761,26788,26744,26747,26769,26764,26762,26749,27446,27443,27447,27448,27537,27535,27533,27534,27532,27690,28096,28075,28084,28083,28276,28076,28137,28130,28087,28150,28116,28160,28104,28128,28127,28118,28094,28133,28124,28125,28123,28148,28106,28093,28141,28144,28090,28117,28098,28111,28105,28112,28146,28115,28157,28119,28109,28131,28091,28922,28941,28919,28951,28916,28940,28912,28932,28915,28944,28924,28927,28934,28947,28928,28920,28918,28939,28930,28942,29310,29307,29308,29311,29469,29463,29447,29457,29464,29450,29448,29439,29455,29470,29576,29686,29688,29685,29700,29697,29693,29703,29696,29690,29692,29695,29708,29707,29684,29704,30052,30051,30158,30162,30159,30155,30156,30161,30160,30351,30345,30419,30521,30511,30509,30513,30514,30516,30515,30525,30501,30523,30517,30792,30802,30793,30797,30794,30796,30758,30789,30800,31076,31079,31081,31082,31075,31083,31073,31163,31226,31224,31222,31223,31375,31380,31376,31541,31559,31540,31525,31536,31522,31524,31539,31512,31530,31517,31537,31531,31533,31535,31538,31544,31514,31523,31892,31896,31894,31907,32053,32061,32056,32054,32058,32069,32044,32041,32065,32071,32062,32063,32074,32059,32040,32611,32661,32668,32669,32667,32714,32715,32717,32720,32721,32711,32719,32713,32799,32798,32795,32839,32835,32840,33048,33061,33049,33051,33069,33055,33068,33054,33057,33045,33063,33053,33058,33297,33336,33331,33338,33332,33330,33396,33680,33699,33704,33677,33658,33651,33700,33652,33679,33665,33685,33689,33653,33684,33705,33661,33667,33676,33693,33691,33706,33675,33662,33701,33711,33672,33687,33712,33663,33702,33671,33710,33654,33690,34393,34390,34495,34487,34498,34497,34501,34490,34480,34504,34489,34483,34488,34508,34484,34491,34492,34499,34493,34494,34898,34953,34965,34984,34978,34986,34970,34961,34977,34975,34968,34983,34969,34971,34967,34980,34988,34956,34963,34958,35202,35286,35289,35285,35376,35367,35372,35358,35897,35899,35932,35933,35965,36005,36221,36219,36217,36284,36290,36281,36287,36289,36568,36574,36573,36572,36567,36576,36577,36900,36875,36881,36892,36876,36897,37103,37098,37104,37108,37106,37107,37076,37099,37100,37097,37206,37208,37210,37203,37205,37356,37364,37361,37363,37368,37348,37369,37354,37355,37367,37352,37358,38266,38278,38280,38524,38509,38507,38513,38511,38591,38762,38916,39141,39319,20635,20629,20628,20638,20619,20643,20611,20620,20622,20637,20584,20636,20626,20610,20615,20831,20948,21266,21265,21412,21415,21905,21928,21925,21933,21879,22085,21922,21907,21896,21903,21941,21889,21923,21906,21924,21885,21900,21926,21887,21909,21921,21902,22284,22569,22583,22553,22558,22567,22563,22568,22517,22600,22565,22556,22555,22579,22591,22582,22574,22585,22584,22573,22572,22587,22881,23215,23188,23199,23162,23202,23198,23160,23206,23164,23205,23212,23189,23214,23095,23172,23178,23191,23171,23179,23209,23163,23165,23180,23196,23183,23187,23197,23530,23501,23499,23508,23505,23498,23502,23564,23600,23863,23875,23915,23873,23883,23871,23861,23889,23886,23893,23859,23866,23890,23869,23857,23897,23874,23865,23881,23864,23868,23858,23862,23872,23877,24132,24129,24408,24486,24485,24491,24777,24761,24780,24802,24782,24772,24852,24818,24842,24854,24837,24821,24851,24824,24828,24830,24769,24835,24856,24861,24848,24831,24836,24843,25162,25492,25521,25520,25550,25573,25576,25583,25539,25757,25587,25546,25568,25590,25557,25586,25589,25697,25567,25534,25565,25564,25540,25560,25555,25538,25543,25548,25547,25544,25584,25559,25561,25906,25959,25962,25956,25948,25960,25957,25996,26013,26014,26030,26064,26066,26236,26220,26235,26240,26225,26233,26218,26226,26369,26892,26835,26884,26844,26922,26860,26858,26865,26895,26838,26871,26859,26852,26870,26899,26896,26867,26849,26887,26828,26888,26992,26804,26897,26863,26822,26900,26872,26832,26877,26876,26856,26891,26890,26903,26830,26824,26845,26846,26854,26868,26833,26886,26836,26857,26901,26917,26823,27449,27451,27455,27452,27540,27543,27545,27541,27581,27632,27634,27635,27696,28156,28230,28231,28191,28233,28296,28220,28221,28229,28258,28203,28223,28225,28253,28275,28188,28211,28235,28224,28241,28219,28163,28206,28254,28264,28252,28257,28209,28200,28256,28273,28267,28217,28194,28208,28243,28261,28199,28280,28260,28279,28245,28281,28242,28262,28213,28214,28250,28960,28958,28975,28923,28974,28977,28963,28965,28962,28978,28959,28968,28986,28955,29259,29274,29320,29321,29318,29317,29323,29458,29451,29488,29474,29489,29491,29479,29490,29485,29478,29475,29493,29452,29742,29740,29744,29739,29718,29722,29729,29741,29745,29732,29731,29725,29737,29728,29746,29947,29999,30063,30060,30183,30170,30177,30182,30173,30175,30180,30167,30357,30354,30426,30534,30535,30532,30541,30533,30538,30542,30539,30540,30686,30700,30816,30820,30821,30812,30829,30833,30826,30830,30832,30825,30824,30814,30818,31092,31091,31090,31088,31234,31242,31235,31244,31236,31385,31462,31460,31562,31547,31556,31560,31564,31566,31552,31576,31557,31906,31902,31912,31905,32088,32111,32099,32083,32086,32103,32106,32079,32109,32092,32107,32082,32084,32105,32081,32095,32078,32574,32575,32613,32614,32674,32672,32673,32727,32849,32847,32848,33022,32980,33091,33098,33106,33103,33095,33085,33101,33082,33254,33262,33271,33272,33273,33284,33340,33341,33343,33397,33595,33743,33785,33827,33728,33768,33810,33767,33764,33788,33782,33808,33734,33736,33771,33763,33727,33793,33757,33765,33752,33791,33761,33739,33742,33750,33781,33737,33801,33807,33758,33809,33798,33730,33779,33749,33786,33735,33745,33770,33811,33731,33772,33774,33732,33787,33751,33762,33819,33755,33790,34520,34530,34534,34515,34531,34522,34538,34525,34539,34524,34540,34537,34519,34536,34513,34888,34902,34901,35002,35031,35001,35000,35008,35006,34998,35004,34999,35005,34994,35073,35017,35221,35224,35223,35293,35290,35291,35406,35405,35385,35417,35392,35415,35416,35396,35397,35410,35400,35409,35402,35404,35407,35935,35969,35968,36026,36030,36016,36025,36021,36228,36224,36233,36312,36307,36301,36295,36310,36316,36303,36309,36313,36296,36311,36293,36591,36599,36602,36601,36582,36590,36581,36597,36583,36584,36598,36587,36593,36588,36596,36585,36909,36916,36911,37126,37164,37124,37119,37116,37128,37113,37115,37121,37120,37127,37125,37123,37217,37220,37215,37218,37216,37377,37386,37413,37379,37402,37414,37391,37388,37376,37394,37375,37373,37382,37380,37415,37378,37404,37412,37401,37399,37381,37398,38267,38285,38284,38288,38535,38526,38536,38537,38531,38528,38594,38600,38595,38641,38640,38764,38768,38766,38919,39081,39147,40166,40697,20099,20100,20150,20669,20671,20678,20654,20676,20682,20660,20680,20674,20656,20673,20666,20657,20683,20681,20662,20664,20951,21114,21112,21115,21116,21955,21979,21964,21968,21963,21962,21981,21952,21972,21956,21993,21951,21970,21901,21967,21973,21986,21974,21960,22002,21965,21977,21954,22292,22611,22632,22628,22607,22605,22601,22639,22613,22606,22621,22617,22629,22619,22589,22627,22641,22780,23239,23236,23243,23226,23224,23217,23221,23216,23231,23240,23227,23238,23223,23232,23242,23220,23222,23245,23225,23184,23510,23512,23513,23583,23603,23921,23907,23882,23909,23922,23916,23902,23912,23911,23906,24048,24143,24142,24138,24141,24139,24261,24268,24262,24267,24263,24384,24495,24493,24823,24905,24906,24875,24901,24886,24882,24878,24902,24879,24911,24873,24896,25120,37224,25123,25125,25124,25541,25585,25579,25616,25618,25609,25632,25636,25651,25667,25631,25621,25624,25657,25655,25634,25635,25612,25638,25648,25640,25665,25653,25647,25610,25626,25664,25637,25639,25611,25575,25627,25646,25633,25614,25967,26002,26067,26246,26252,26261,26256,26251,26250,26265,26260,26232,26400,26982,26975,26936,26958,26978,26993,26943,26949,26986,26937,26946,26967,26969,27002,26952,26953,26933,26988,26931,26941,26981,26864,27000,26932,26985,26944,26991,26948,26998,26968,26945,26996,26956,26939,26955,26935,26972,26959,26961,26930,26962,26927,27003,26940,27462,27461,27459,27458,27464,27457,27547,64013,27643,27644,27641,27639,27640,28315,28374,28360,28303,28352,28319,28307,28308,28320,28337,28345,28358,28370,28349,28353,28318,28361,28343,28336,28365,28326,28367,28338,28350,28355,28380,28376,28313,28306,28302,28301,28324,28321,28351,28339,28368,28362,28311,28334,28323,28999,29012,29010,29027,29024,28993,29021,29026,29042,29048,29034,29025,28994,29016,28995,29003,29040,29023,29008,29011,28996,29005,29018,29263,29325,29324,29329,29328,29326,29500,29506,29499,29498,29504,29514,29513,29764,29770,29771,29778,29777,29783,29760,29775,29776,29774,29762,29766,29773,29780,29921,29951,29950,29949,29981,30073,30071,27011,30191,30223,30211,30199,30206,30204,30201,30200,30224,30203,30198,30189,30197,30205,30361,30389,30429,30549,30559,30560,30546,30550,30554,30569,30567,30548,30553,30573,30688,30855,30874,30868,30863,30852,30869,30853,30854,30881,30851,30841,30873,30848,30870,30843,31100,31106,31101,31097,31249,31256,31257,31250,31255,31253,31266,31251,31259,31248,31395,31394,31390,31467,31590,31588,31597,31604,31593,31602,31589,31603,31601,31600,31585,31608,31606,31587,31922,31924,31919,32136,32134,32128,32141,32127,32133,32122,32142,32123,32131,32124,32140,32148,32132,32125,32146,32621,32619,32615,32616,32620,32678,32677,32679,32731,32732,32801,33124,33120,33143,33116,33129,33115,33122,33138,26401,33118,33142,33127,33135,33092,33121,33309,33353,33348,33344,33346,33349,34033,33855,33878,33910,33913,33935,33933,33893,33873,33856,33926,33895,33840,33869,33917,33882,33881,33908,33907,33885,34055,33886,33847,33850,33844,33914,33859,33912,33842,33861,33833,33753,33867,33839,33858,33837,33887,33904,33849,33870,33868,33874,33903,33989,33934,33851,33863,33846,33843,33896,33918,33860,33835,33888,33876,33902,33872,34571,34564,34551,34572,34554,34518,34549,34637,34552,34574,34569,34561,34550,34573,34565,35030,35019,35021,35022,35038,35035,35034,35020,35024,35205,35227,35295,35301,35300,35297,35296,35298,35292,35302,35446,35462,35455,35425,35391,35447,35458,35460,35445,35459,35457,35444,35450,35900,35915,35914,35941,35940,35942,35974,35972,35973,36044,36200,36201,36241,36236,36238,36239,36237,36243,36244,36240,36242,36336,36320,36332,36337,36334,36304,36329,36323,36322,36327,36338,36331,36340,36614,36607,36609,36608,36613,36615,36616,36610,36619,36946,36927,36932,36937,36925,37136,37133,37135,37137,37142,37140,37131,37134,37230,37231,37448,37458,37424,37434,37478,37427,37477,37470,37507,37422,37450,37446,37485,37484,37455,37472,37479,37487,37430,37473,37488,37425,37460,37475,37456,37490,37454,37459,37452,37462,37426,38303,38300,38302,38299,38546,38547,38545,38551,38606,38650,38653,38648,38645,38771,38775,38776,38770,38927,38925,38926,39084,39158,39161,39343,39346,39344,39349,39597,39595,39771,40170,40173,40167,40576,40701,20710,20692,20695,20712,20723,20699,20714,20701,20708,20691,20716,20720,20719,20707,20704,20952,21120,21121,21225,21227,21296,21420,22055,22037,22028,22034,22012,22031,22044,22017,22035,22018,22010,22045,22020,22015,22009,22665,22652,22672,22680,22662,22657,22655,22644,22667,22650,22663,22673,22670,22646,22658,22664,22651,22676,22671,22782,22891,23260,23278,23269,23253,23274,23258,23277,23275,23283,23266,23264,23259,23276,23262,23261,23257,23272,23263,23415,23520,23523,23651,23938,23936,23933,23942,23930,23937,23927,23946,23945,23944,23934,23932,23949,23929,23935,24152,24153,24147,24280,24273,24279,24270,24284,24277,24281,24274,24276,24388,24387,24431,24502,24876,24872,24897,24926,24945,24947,24914,24915,24946,24940,24960,24948,24916,24954,24923,24933,24891,24938,24929,24918,25129,25127,25131,25643,25677,25691,25693,25716,25718,25714,25715,25725,25717,25702,25766,25678,25730,25694,25692,25675,25683,25696,25680,25727,25663,25708,25707,25689,25701,25719,25971,26016,26273,26272,26271,26373,26372,26402,27057,27062,27081,27040,27086,27030,27056,27052,27068,27025,27033,27022,27047,27021,27049,27070,27055,27071,27076,27069,27044,27092,27065,27082,27034,27087,27059,27027,27050,27041,27038,27097,27031,27024,27074,27061,27045,27078,27466,27469,27467,27550,27551,27552,27587,27588,27646,28366,28405,28401,28419,28453,28408,28471,28411,28462,28425,28494,28441,28442,28455,28440,28475,28434,28397,28426,28470,28531,28409,28398,28461,28480,28464,28476,28469,28395,28423,28430,28483,28421,28413,28406,28473,28444,28412,28474,28447,28429,28446,28424,28449,29063,29072,29065,29056,29061,29058,29071,29051,29062,29057,29079,29252,29267,29335,29333,29331,29507,29517,29521,29516,29794,29811,29809,29813,29810,29799,29806,29952,29954,29955,30077,30096,30230,30216,30220,30229,30225,30218,30228,30392,30593,30588,30597,30594,30574,30592,30575,30590,30595,30898,30890,30900,30893,30888,30846,30891,30878,30885,30880,30892,30882,30884,31128,31114,31115,31126,31125,31124,31123,31127,31112,31122,31120,31275,31306,31280,31279,31272,31270,31400,31403,31404,31470,31624,31644,31626,31633,31632,31638,31629,31628,31643,31630,31621,31640,21124,31641,31652,31618,31931,31935,31932,31930,32167,32183,32194,32163,32170,32193,32192,32197,32157,32206,32196,32198,32203,32204,32175,32185,32150,32188,32159,32166,32174,32169,32161,32201,32627,32738,32739,32741,32734,32804,32861,32860,33161,33158,33155,33159,33165,33164,33163,33301,33943,33956,33953,33951,33978,33998,33986,33964,33966,33963,33977,33972,33985,33997,33962,33946,33969,34000,33949,33959,33979,33954,33940,33991,33996,33947,33961,33967,33960,34006,33944,33974,33999,33952,34007,34004,34002,34011,33968,33937,34401,34611,34595,34600,34667,34624,34606,34590,34593,34585,34587,34627,34604,34625,34622,34630,34592,34610,34602,34605,34620,34578,34618,34609,34613,34626,34598,34599,34616,34596,34586,34608,34577,35063,35047,35057,35058,35066,35070,35054,35068,35062,35067,35056,35052,35051,35229,35233,35231,35230,35305,35307,35304,35499,35481,35467,35474,35471,35478,35901,35944,35945,36053,36047,36055,36246,36361,36354,36351,36365,36349,36362,36355,36359,36358,36357,36350,36352,36356,36624,36625,36622,36621,37155,37148,37152,37154,37151,37149,37146,37156,37153,37147,37242,37234,37241,37235,37541,37540,37494,37531,37498,37536,37524,37546,37517,37542,37530,37547,37497,37527,37503,37539,37614,37518,37506,37525,37538,37501,37512,37537,37514,37510,37516,37529,37543,37502,37511,37545,37533,37515,37421,38558,38561,38655,38744,38781,38778,38782,38787,38784,38786,38779,38788,38785,38783,38862,38861,38934,39085,39086,39170,39168,39175,39325,39324,39363,39353,39355,39354,39362,39357,39367,39601,39651,39655,39742,39743,39776,39777,39775,40177,40178,40181,40615,20735,20739,20784,20728,20742,20743,20726,20734,20747,20748,20733,20746,21131,21132,21233,21231,22088,22082,22092,22069,22081,22090,22089,22086,22104,22106,22080,22067,22077,22060,22078,22072,22058,22074,22298,22699,22685,22705,22688,22691,22703,22700,22693,22689,22783,23295,23284,23293,23287,23286,23299,23288,23298,23289,23297,23303,23301,23311,23655,23961,23959,23967,23954,23970,23955,23957,23968,23964,23969,23962,23966,24169,24157,24160,24156,32243,24283,24286,24289,24393,24498,24971,24963,24953,25009,25008,24994,24969,24987,24979,25007,25005,24991,24978,25002,24993,24973,24934,25011,25133,25710,25712,25750,25760,25733,25751,25756,25743,25739,25738,25740,25763,25759,25704,25777,25752,25974,25978,25977,25979,26034,26035,26293,26288,26281,26290,26295,26282,26287,27136,27142,27159,27109,27128,27157,27121,27108,27168,27135,27116,27106,27163,27165,27134,27175,27122,27118,27156,27127,27111,27200,27144,27110,27131,27149,27132,27115,27145,27140,27160,27173,27151,27126,27174,27143,27124,27158,27473,27557,27555,27554,27558,27649,27648,27647,27650,28481,28454,28542,28551,28614,28562,28557,28553,28556,28514,28495,28549,28506,28566,28534,28524,28546,28501,28530,28498,28496,28503,28564,28563,28509,28416,28513,28523,28541,28519,28560,28499,28555,28521,28543,28565,28515,28535,28522,28539,29106,29103,29083,29104,29088,29082,29097,29109,29085,29093,29086,29092,29089,29098,29084,29095,29107,29336,29338,29528,29522,29534,29535,29536,29533,29531,29537,29530,29529,29538,29831,29833,29834,29830,29825,29821,29829,29832,29820,29817,29960,29959,30078,30245,30238,30233,30237,30236,30243,30234,30248,30235,30364,30365,30366,30363,30605,30607,30601,30600,30925,30907,30927,30924,30929,30926,30932,30920,30915,30916,30921,31130,31137,31136,31132,31138,31131,27510,31289,31410,31412,31411,31671,31691,31678,31660,31694,31663,31673,31690,31669,31941,31944,31948,31947,32247,32219,32234,32231,32215,32225,32259,32250,32230,32246,32241,32240,32238,32223,32630,32684,32688,32685,32749,32747,32746,32748,32742,32744,32868,32871,33187,33183,33182,33173,33186,33177,33175,33302,33359,33363,33362,33360,33358,33361,34084,34107,34063,34048,34089,34062,34057,34061,34079,34058,34087,34076,34043,34091,34042,34056,34060,34036,34090,34034,34069,34039,34027,34035,34044,34066,34026,34025,34070,34046,34088,34077,34094,34050,34045,34078,34038,34097,34086,34023,34024,34032,34031,34041,34072,34080,34096,34059,34073,34095,34402,34646,34659,34660,34679,34785,34675,34648,34644,34651,34642,34657,34650,34641,34654,34669,34666,34640,34638,34655,34653,34671,34668,34682,34670,34652,34661,34639,34683,34677,34658,34663,34665,34906,35077,35084,35092,35083,35095,35096,35097,35078,35094,35089,35086,35081,35234,35236,35235,35309,35312,35308,35535,35526,35512,35539,35537,35540,35541,35515,35543,35518,35520,35525,35544,35523,35514,35517,35545,35902,35917,35983,36069,36063,36057,36072,36058,36061,36071,36256,36252,36257,36251,36384,36387,36389,36388,36398,36373,36379,36374,36369,36377,36390,36391,36372,36370,36376,36371,36380,36375,36378,36652,36644,36632,36634,36640,36643,36630,36631,36979,36976,36975,36967,36971,37167,37163,37161,37162,37170,37158,37166,37253,37254,37258,37249,37250,37252,37248,37584,37571,37572,37568,37593,37558,37583,37617,37599,37592,37609,37591,37597,37580,37615,37570,37608,37578,37576,37582,37606,37581,37589,37577,37600,37598,37607,37585,37587,37557,37601,37574,37556,38268,38316,38315,38318,38320,38564,38562,38611,38661,38664,38658,38746,38794,38798,38792,38864,38863,38942,38941,38950,38953,38952,38944,38939,38951,39090,39176,39162,39185,39188,39190,39191,39189,39388,39373,39375,39379,39380,39374,39369,39382,39384,39371,39383,39372,39603,39660,39659,39667,39666,39665,39750,39747,39783,39796,39793,39782,39798,39797,39792,39784,39780,39788,40188,40186,40189,40191,40183,40199,40192,40185,40187,40200,40197,40196,40579,40659,40719,40720,20764,20755,20759,20762,20753,20958,21300,21473,22128,22112,22126,22131,22118,22115,22125,22130,22110,22135,22300,22299,22728,22717,22729,22719,22714,22722,22716,22726,23319,23321,23323,23329,23316,23315,23312,23318,23336,23322,23328,23326,23535,23980,23985,23977,23975,23989,23984,23982,23978,23976,23986,23981,23983,23988,24167,24168,24166,24175,24297,24295,24294,24296,24293,24395,24508,24989,25000,24982,25029,25012,25030,25025,25036,25018,25023,25016,24972,25815,25814,25808,25807,25801,25789,25737,25795,25819,25843,25817,25907,25983,25980,26018,26312,26302,26304,26314,26315,26319,26301,26299,26298,26316,26403,27188,27238,27209,27239,27186,27240,27198,27229,27245,27254,27227,27217,27176,27226,27195,27199,27201,27242,27236,27216,27215,27220,27247,27241,27232,27196,27230,27222,27221,27213,27214,27206,27477,27476,27478,27559,27562,27563,27592,27591,27652,27651,27654,28589,28619,28579,28615,28604,28622,28616,28510,28612,28605,28574,28618,28584,28676,28581,28590,28602,28588,28586,28623,28607,28600,28578,28617,28587,28621,28591,28594,28592,29125,29122,29119,29112,29142,29120,29121,29131,29140,29130,29127,29135,29117,29144,29116,29126,29146,29147,29341,29342,29545,29542,29543,29548,29541,29547,29546,29823,29850,29856,29844,29842,29845,29857,29963,30080,30255,30253,30257,30269,30259,30268,30261,30258,30256,30395,30438,30618,30621,30625,30620,30619,30626,30627,30613,30617,30615,30941,30953,30949,30954,30942,30947,30939,30945,30946,30957,30943,30944,31140,31300,31304,31303,31414,31416,31413,31409,31415,31710,31715,31719,31709,31701,31717,31706,31720,31737,31700,31722,31714,31708,31723,31704,31711,31954,31956,31959,31952,31953,32274,32289,32279,32268,32287,32288,32275,32270,32284,32277,32282,32290,32267,32271,32278,32269,32276,32293,32292,32579,32635,32636,32634,32689,32751,32810,32809,32876,33201,33190,33198,33209,33205,33195,33200,33196,33204,33202,33207,33191,33266,33365,33366,33367,34134,34117,34155,34125,34131,34145,34136,34112,34118,34148,34113,34146,34116,34129,34119,34147,34110,34139,34161,34126,34158,34165,34133,34151,34144,34188,34150,34141,34132,34149,34156,34403,34405,34404,34715,34703,34711,34707,34706,34696,34689,34710,34712,34681,34695,34723,34693,34704,34705,34717,34692,34708,34716,34714,34697,35102,35110,35120,35117,35118,35111,35121,35106,35113,35107,35119,35116,35103,35313,35552,35554,35570,35572,35573,35549,35604,35556,35551,35568,35528,35550,35553,35560,35583,35567,35579,35985,35986,35984,36085,36078,36081,36080,36083,36204,36206,36261,36263,36403,36414,36408,36416,36421,36406,36412,36413,36417,36400,36415,36541,36662,36654,36661,36658,36665,36663,36660,36982,36985,36987,36998,37114,37171,37173,37174,37267,37264,37265,37261,37263,37671,37662,37640,37663,37638,37647,37754,37688,37692,37659,37667,37650,37633,37702,37677,37646,37645,37579,37661,37626,37669,37651,37625,37623,37684,37634,37668,37631,37673,37689,37685,37674,37652,37644,37643,37630,37641,37632,37627,37654,38332,38349,38334,38329,38330,38326,38335,38325,38333,38569,38612,38667,38674,38672,38809,38807,38804,38896,38904,38965,38959,38962,39204,39199,39207,39209,39326,39406,39404,39397,39396,39408,39395,39402,39401,39399,39609,39615,39604,39611,39670,39674,39673,39671,39731,39808,39813,39815,39804,39806,39803,39810,39827,39826,39824,39802,39829,39805,39816,40229,40215,40224,40222,40212,40233,40221,40216,40226,40208,40217,40223,40584,40582,40583,40622,40621,40661,40662,40698,40722,40765,20774,20773,20770,20772,20768,20777,21236,22163,22156,22157,22150,22148,22147,22142,22146,22143,22145,22742,22740,22735,22738,23341,23333,23346,23331,23340,23335,23334,23343,23342,23419,23537,23538,23991,24172,24170,24510,24507,25027,25013,25020,25063,25056,25061,25060,25064,25054,25839,25833,25827,25835,25828,25832,25985,25984,26038,26074,26322,27277,27286,27265,27301,27273,27295,27291,27297,27294,27271,27283,27278,27285,27267,27304,27300,27281,27263,27302,27290,27269,27276,27282,27483,27565,27657,28620,28585,28660,28628,28643,28636,28653,28647,28646,28638,28658,28637,28642,28648,29153,29169,29160,29170,29156,29168,29154,29555,29550,29551,29847,29874,29867,29840,29866,29869,29873,29861,29871,29968,29969,29970,29967,30084,30275,30280,30281,30279,30372,30441,30645,30635,30642,30647,30646,30644,30641,30632,30704,30963,30973,30978,30971,30972,30962,30981,30969,30974,30980,31147,31144,31324,31323,31318,31320,31316,31322,31422,31424,31425,31749,31759,31730,31744,31743,31739,31758,31732,31755,31731,31746,31753,31747,31745,31736,31741,31750,31728,31729,31760,31754,31976,32301,32316,32322,32307,38984,32312,32298,32329,32320,32327,32297,32332,32304,32315,32310,32324,32314,32581,32639,32638,32637,32756,32754,32812,33211,33220,33228,33226,33221,33223,33212,33257,33371,33370,33372,34179,34176,34191,34215,34197,34208,34187,34211,34171,34212,34202,34206,34167,34172,34185,34209,34170,34168,34135,34190,34198,34182,34189,34201,34205,34177,34210,34178,34184,34181,34169,34166,34200,34192,34207,34408,34750,34730,34733,34757,34736,34732,34745,34741,34748,34734,34761,34755,34754,34764,34743,34735,34756,34762,34740,34742,34751,34744,34749,34782,34738,35125,35123,35132,35134,35137,35154,35127,35138,35245,35247,35246,35314,35315,35614,35608,35606,35601,35589,35595,35618,35599,35602,35605,35591,35597,35592,35590,35612,35603,35610,35919,35952,35954,35953,35951,35989,35988,36089,36207,36430,36429,36435,36432,36428,36423,36675,36672,36997,36990,37176,37274,37282,37275,37273,37279,37281,37277,37280,37793,37763,37807,37732,37718,37703,37756,37720,37724,37750,37705,37712,37713,37728,37741,37775,37708,37738,37753,37719,37717,37714,37711,37745,37751,37755,37729,37726,37731,37735,37760,37710,37721,38343,38336,38345,38339,38341,38327,38574,38576,38572,38688,38687,38680,38685,38681,38810,38817,38812,38814,38813,38869,38868,38897,38977,38980,38986,38985,38981,38979,39205,39211,39212,39210,39219,39218,39215,39213,39217,39216,39320,39331,39329,39426,39418,39412,39415,39417,39416,39414,39419,39421,39422,39420,39427,39614,39678,39677,39681,39676,39752,39834,39848,39838,39835,39846,39841,39845,39844,39814,39842,39840,39855,40243,40257,40295,40246,40238,40239,40241,40248,40240,40261,40258,40259,40254,40247,40256,40253,32757,40237,40586,40585,40589,40624,40648,40666,40699,40703,40740,40739,40738,40788,40864,20785,20781,20782,22168,22172,22167,22170,22173,22169,22896,23356,23657,23658,24000,24173,24174,25048,25055,25069,25070,25073,25066,25072,25067,25046,25065,25855,25860,25853,25848,25857,25859,25852,26004,26075,26330,26331,26328,27333,27321,27325,27361,27334,27322,27318,27319,27335,27316,27309,27486,27593,27659,28679,28684,28685,28673,28677,28692,28686,28671,28672,28667,28710,28668,28663,28682,29185,29183,29177,29187,29181,29558,29880,29888,29877,29889,29886,29878,29883,29890,29972,29971,30300,30308,30297,30288,30291,30295,30298,30374,30397,30444,30658,30650,30975,30988,30995,30996,30985,30992,30994,30993,31149,31148,31327,31772,31785,31769,31776,31775,31789,31773,31782,31784,31778,31781,31792,32348,32336,32342,32355,32344,32354,32351,32337,32352,32343,32339,32693,32691,32759,32760,32885,33233,33234,33232,33375,33374,34228,34246,34240,34243,34242,34227,34229,34237,34247,34244,34239,34251,34254,34248,34245,34225,34230,34258,34340,34232,34231,34238,34409,34791,34790,34786,34779,34795,34794,34789,34783,34803,34788,34772,34780,34771,34797,34776,34787,34724,34775,34777,34817,34804,34792,34781,35155,35147,35151,35148,35142,35152,35153,35145,35626,35623,35619,35635,35632,35637,35655,35631,35644,35646,35633,35621,35639,35622,35638,35630,35620,35643,35645,35642,35906,35957,35993,35992,35991,36094,36100,36098,36096,36444,36450,36448,36439,36438,36446,36453,36455,36443,36442,36449,36445,36457,36436,36678,36679,36680,36683,37160,37178,37179,37182,37288,37285,37287,37295,37290,37813,37772,37778,37815,37787,37789,37769,37799,37774,37802,37790,37798,37781,37768,37785,37791,37773,37809,37777,37810,37796,37800,37812,37795,37797,38354,38355,38353,38579,38615,38618,24002,38623,38616,38621,38691,38690,38693,38828,38830,38824,38827,38820,38826,38818,38821,38871,38873,38870,38872,38906,38992,38993,38994,39096,39233,39228,39226,39439,39435,39433,39437,39428,39441,39434,39429,39431,39430,39616,39644,39688,39684,39685,39721,39733,39754,39756,39755,39879,39878,39875,39871,39873,39861,39864,39891,39862,39876,39865,39869,40284,40275,40271,40266,40283,40267,40281,40278,40268,40279,40274,40276,40287,40280,40282,40590,40588,40671,40705,40704,40726,40741,40747,40746,40745,40744,40780,40789,20788,20789,21142,21239,21428,22187,22189,22182,22183,22186,22188,22746,22749,22747,22802,23357,23358,23359,24003,24176,24511,25083,25863,25872,25869,25865,25868,25870,25988,26078,26077,26334,27367,27360,27340,27345,27353,27339,27359,27356,27344,27371,27343,27341,27358,27488,27568,27660,28697,28711,28704,28694,28715,28705,28706,28707,28713,28695,28708,28700,28714,29196,29194,29191,29186,29189,29349,29350,29348,29347,29345,29899,29893,29879,29891,29974,30304,30665,30666,30660,30705,31005,31003,31009,31004,30999,31006,31152,31335,31336,31795,31804,31801,31788,31803,31980,31978,32374,32373,32376,32368,32375,32367,32378,32370,32372,32360,32587,32586,32643,32646,32695,32765,32766,32888,33239,33237,33380,33377,33379,34283,34289,34285,34265,34273,34280,34266,34263,34284,34290,34296,34264,34271,34275,34268,34257,34288,34278,34287,34270,34274,34816,34810,34819,34806,34807,34825,34828,34827,34822,34812,34824,34815,34826,34818,35170,35162,35163,35159,35169,35164,35160,35165,35161,35208,35255,35254,35318,35664,35656,35658,35648,35667,35670,35668,35659,35669,35665,35650,35666,35671,35907,35959,35958,35994,36102,36103,36105,36268,36266,36269,36267,36461,36472,36467,36458,36463,36475,36546,36690,36689,36687,36688,36691,36788,37184,37183,37296,37293,37854,37831,37839,37826,37850,37840,37881,37868,37836,37849,37801,37862,37834,37844,37870,37859,37845,37828,37838,37824,37842,37863,38269,38362,38363,38625,38697,38699,38700,38696,38694,38835,38839,38838,38877,38878,38879,39004,39001,39005,38999,39103,39101,39099,39102,39240,39239,39235,39334,39335,39450,39445,39461,39453,39460,39451,39458,39456,39463,39459,39454,39452,39444,39618,39691,39690,39694,39692,39735,39914,39915,39904,39902,39908,39910,39906,39920,39892,39895,39916,39900,39897,39909,39893,39905,39898,40311,40321,40330,40324,40328,40305,40320,40312,40326,40331,40332,40317,40299,40308,40309,40304,40297,40325,40307,40315,40322,40303,40313,40319,40327,40296,40596,40593,40640,40700,40749,40768,40769,40781,40790,40791,40792,21303,22194,22197,22195,22755,23365,24006,24007,24302,24303,24512,24513,25081,25879,25878,25877,25875,26079,26344,26339,26340,27379,27376,27370,27368,27385,27377,27374,27375,28732,28725,28719,28727,28724,28721,28738,28728,28735,28730,28729,28736,28731,28723,28737,29203,29204,29352,29565,29564,29882,30379,30378,30398,30445,30668,30670,30671,30669,30706,31013,31011,31015,31016,31012,31017,31154,31342,31340,31341,31479,31817,31816,31818,31815,31813,31982,32379,32382,32385,32384,32698,32767,32889,33243,33241,33291,33384,33385,34338,34303,34305,34302,34331,34304,34294,34308,34313,34309,34316,34301,34841,34832,34833,34839,34835,34838,35171,35174,35257,35319,35680,35690,35677,35688,35683,35685,35687,35693,36270,36486,36488,36484,36697,36694,36695,36693,36696,36698,37005,37187,37185,37303,37301,37298,37299,37899,37907,37883,37920,37903,37908,37886,37909,37904,37928,37913,37901,37877,37888,37879,37895,37902,37910,37906,37882,37897,37880,37898,37887,37884,37900,37878,37905,37894,38366,38368,38367,38702,38703,38841,38843,38909,38910,39008,39010,39011,39007,39105,39106,39248,39246,39257,39244,39243,39251,39474,39476,39473,39468,39466,39478,39465,39470,39480,39469,39623,39626,39622,39696,39698,39697,39947,39944,39927,39941,39954,39928,40000,39943,39950,39942,39959,39956,39945,40351,40345,40356,40349,40338,40344,40336,40347,40352,40340,40348,40362,40343,40353,40346,40354,40360,40350,40355,40383,40361,40342,40358,40359,40601,40603,40602,40677,40676,40679,40678,40752,40750,40795,40800,40798,40797,40793,40849,20794,20793,21144,21143,22211,22205,22206,23368,23367,24011,24015,24305,25085,25883,27394,27388,27395,27384,27392,28739,28740,28746,28744,28745,28741,28742,29213,29210,29209,29566,29975,30314,30672,31021,31025,31023,31828,31827,31986,32394,32391,32392,32395,32390,32397,32589,32699,32816,33245,34328,34346,34342,34335,34339,34332,34329,34343,34350,34337,34336,34345,34334,34341,34857,34845,34843,34848,34852,34844,34859,34890,35181,35177,35182,35179,35322,35705,35704,35653,35706,35707,36112,36116,36271,36494,36492,36702,36699,36701,37190,37188,37189,37305,37951,37947,37942,37929,37949,37948,37936,37945,37930,37943,37932,37952,37937,38373,38372,38371,38709,38714,38847,38881,39012,39113,39110,39104,39256,39254,39481,39485,39494,39492,39490,39489,39482,39487,39629,39701,39703,39704,39702,39738,39762,39979,39965,39964,39980,39971,39976,39977,39972,39969,40375,40374,40380,40385,40391,40394,40399,40382,40389,40387,40379,40373,40398,40377,40378,40364,40392,40369,40365,40396,40371,40397,40370,40570,40604,40683,40686,40685,40731,40728,40730,40753,40782,40805,40804,40850,20153,22214,22213,22219,22897,23371,23372,24021,24017,24306,25889,25888,25894,25890,27403,27400,27401,27661,28757,28758,28759,28754,29214,29215,29353,29567,29912,29909,29913,29911,30317,30381,31029,31156,31344,31345,31831,31836,31833,31835,31834,31988,31985,32401,32591,32647,33246,33387,34356,34357,34355,34348,34354,34358,34860,34856,34854,34858,34853,35185,35263,35262,35323,35710,35716,35714,35718,35717,35711,36117,36501,36500,36506,36498,36496,36502,36503,36704,36706,37191,37964,37968,37962,37963,37967,37959,37957,37960,37961,37958,38719,38883,39018,39017,39115,39252,39259,39502,39507,39508,39500,39503,39496,39498,39497,39506,39504,39632,39705,39723,39739,39766,39765,40006,40008,39999,40004,39993,39987,40001,39996,39991,39988,39986,39997,39990,40411,40402,40414,40410,40395,40400,40412,40401,40415,40425,40409,40408,40406,40437,40405,40413,40630,40688,40757,40755,40754,40770,40811,40853,40866,20797,21145,22760,22759,22898,23373,24024,34863,24399,25089,25091,25092,25897,25893,26006,26347,27409,27410,27407,27594,28763,28762,29218,29570,29569,29571,30320,30676,31847,31846,32405,33388,34362,34368,34361,34364,34353,34363,34366,34864,34866,34862,34867,35190,35188,35187,35326,35724,35726,35723,35720,35909,36121,36504,36708,36707,37308,37986,37973,37981,37975,37982,38852,38853,38912,39510,39513,39710,39711,39712,40018,40024,40016,40010,40013,40011,40021,40025,40012,40014,40443,40439,40431,40419,40427,40440,40420,40438,40417,40430,40422,40434,40432,40418,40428,40436,40435,40424,40429,40642,40656,40690,40691,40710,40732,40760,40759,40758,40771,40783,40817,40816,40814,40815,22227,22221,23374,23661,25901,26349,26350,27411,28767,28769,28765,28768,29219,29915,29925,30677,31032,31159,31158,31850,32407,32649,33389,34371,34872,34871,34869,34891,35732,35733,36510,36511,36512,36509,37310,37309,37314,37995,37992,37993,38629,38726,38723,38727,38855,38885,39518,39637,39769,40035,40039,40038,40034,40030,40032,40450,40446,40455,40451,40454,40453,40448,40449,40457,40447,40445,40452,40608,40734,40774,40820,40821,40822,22228,25902,26040,27416,27417,27415,27418,28770,29222,29354,30680,30681,31033,31849,31851,31990,32410,32408,32411,32409,33248,33249,34374,34375,34376,35193,35194,35196,35195,35327,35736,35737,36517,36516,36515,37998,37997,37999,38001,38003,38729,39026,39263,40040,40046,40045,40459,40461,40464,40463,40466,40465,40609,40693,40713,40775,40824,40827,40826,40825,22302,28774,31855,34876,36274,36518,37315,38004,38008,38006,38005,39520,40052,40051,40049,40053,40468,40467,40694,40714,40868,28776,28773,31991,34410,34878,34877,34879,35742,35996,36521,36553,38731,39027,39028,39116,39265,39339,39524,39526,39527,39716,40469,40471,40776,25095,27422,29223,34380,36520,38018,38016,38017,39529,39528,39726,40473,29225,34379,35743,38019,40057,40631,30325,39531,40058,40477,28777,28778,40612,40830,40777,40856,30849,37561,35023,22715,24658,31911,23290,9556,9574,9559,9568,9580,9571,9562,9577,9565,9554,9572,9557,9566,9578,9569,9560,9575,9563,9555,9573,9558,9567,9579,9570,9561,9576,9564,9553,9552,9581,9582,9584,9583,65517,132423,37595,132575,147397,34124,17077,29679,20917,13897,149826,166372,37700,137691,33518,146632,30780,26436,25311,149811,166314,131744,158643,135941,20395,140525,20488,159017,162436,144896,150193,140563,20521,131966,24484,131968,131911,28379,132127,20605,20737,13434,20750,39020,14147,33814,149924,132231,20832,144308,20842,134143,139516,131813,140592,132494,143923,137603,23426,34685,132531,146585,20914,20920,40244,20937,20943,20945,15580,20947,150182,20915,20962,21314,20973,33741,26942,145197,24443,21003,21030,21052,21173,21079,21140,21177,21189,31765,34114,21216,34317,158483,21253,166622,21833,28377,147328,133460,147436,21299,21316,134114,27851,136998,26651,29653,24650,16042,14540,136936,29149,17570,21357,21364,165547,21374,21375,136598,136723,30694,21395,166555,21408,21419,21422,29607,153458,16217,29596,21441,21445,27721,20041,22526,21465,15019,134031,21472,147435,142755,21494,134263,21523,28793,21803,26199,27995,21613,158547,134516,21853,21647,21668,18342,136973,134877,15796,134477,166332,140952,21831,19693,21551,29719,21894,21929,22021,137431,147514,17746,148533,26291,135348,22071,26317,144010,26276,26285,22093,22095,30961,22257,38791,21502,22272,22255,22253,166758,13859,135759,22342,147877,27758,28811,22338,14001,158846,22502,136214,22531,136276,148323,22566,150517,22620,22698,13665,22752,22748,135740,22779,23551,22339,172368,148088,37843,13729,22815,26790,14019,28249,136766,23076,21843,136850,34053,22985,134478,158849,159018,137180,23001,137211,137138,159142,28017,137256,136917,23033,159301,23211,23139,14054,149929,23159,14088,23190,29797,23251,159649,140628,15749,137489,14130,136888,24195,21200,23414,25992,23420,162318,16388,18525,131588,23509,24928,137780,154060,132517,23539,23453,19728,23557,138052,23571,29646,23572,138405,158504,23625,18653,23685,23785,23791,23947,138745,138807,23824,23832,23878,138916,23738,24023,33532,14381,149761,139337,139635,33415,14390,15298,24110,27274,24181,24186,148668,134355,21414,20151,24272,21416,137073,24073,24308,164994,24313,24315,14496,24316,26686,37915,24333,131521,194708,15070,18606,135994,24378,157832,140240,24408,140401,24419,38845,159342,24434,37696,166454,24487,23990,15711,152144,139114,159992,140904,37334,131742,166441,24625,26245,137335,14691,15815,13881,22416,141236,31089,15936,24734,24740,24755,149890,149903,162387,29860,20705,23200,24932,33828,24898,194726,159442,24961,20980,132694,24967,23466,147383,141407,25043,166813,170333,25040,14642,141696,141505,24611,24924,25886,25483,131352,25285,137072,25301,142861,25452,149983,14871,25656,25592,136078,137212,25744,28554,142902,38932,147596,153373,25825,25829,38011,14950,25658,14935,25933,28438,150056,150051,25989,25965,25951,143486,26037,149824,19255,26065,16600,137257,26080,26083,24543,144384,26136,143863,143864,26180,143780,143781,26187,134773,26215,152038,26227,26228,138813,143921,165364,143816,152339,30661,141559,39332,26370,148380,150049,15147,27130,145346,26462,26471,26466,147917,168173,26583,17641,26658,28240,37436,26625,144358,159136,26717,144495,27105,27147,166623,26995,26819,144845,26881,26880,15666,14849,144956,15232,26540,26977,166474,17148,26934,27032,15265,132041,33635,20624,27129,144985,139562,27205,145155,27293,15347,26545,27336,168348,15373,27421,133411,24798,27445,27508,141261,28341,146139,132021,137560,14144,21537,146266,27617,147196,27612,27703,140427,149745,158545,27738,33318,27769,146876,17605,146877,147876,149772,149760,146633,14053,15595,134450,39811,143865,140433,32655,26679,159013,159137,159211,28054,27996,28284,28420,149887,147589,159346,34099,159604,20935,27804,28189,33838,166689,28207,146991,29779,147330,31180,28239,23185,143435,28664,14093,28573,146992,28410,136343,147517,17749,37872,28484,28508,15694,28532,168304,15675,28575,147780,28627,147601,147797,147513,147440,147380,147775,20959,147798,147799,147776,156125,28747,28798,28839,28801,28876,28885,28886,28895,16644,15848,29108,29078,148087,28971,28997,23176,29002,29038,23708,148325,29007,37730,148161,28972,148570,150055,150050,29114,166888,28861,29198,37954,29205,22801,37955,29220,37697,153093,29230,29248,149876,26813,29269,29271,15957,143428,26637,28477,29314,29482,29483,149539,165931,18669,165892,29480,29486,29647,29610,134202,158254,29641,29769,147938,136935,150052,26147,14021,149943,149901,150011,29687,29717,26883,150054,29753,132547,16087,29788,141485,29792,167602,29767,29668,29814,33721,29804,14128,29812,37873,27180,29826,18771,150156,147807,150137,166799,23366,166915,137374,29896,137608,29966,29929,29982,167641,137803,23511,167596,37765,30029,30026,30055,30062,151426,16132,150803,30094,29789,30110,30132,30210,30252,30289,30287,30319,30326,156661,30352,33263,14328,157969,157966,30369,30373,30391,30412,159647,33890,151709,151933,138780,30494,30502,30528,25775,152096,30552,144044,30639,166244,166248,136897,30708,30729,136054,150034,26826,30895,30919,30931,38565,31022,153056,30935,31028,30897,161292,36792,34948,166699,155779,140828,31110,35072,26882,31104,153687,31133,162617,31036,31145,28202,160038,16040,31174,168205,31188], + "euc-kr":[44034,44035,44037,44038,44043,44044,44045,44046,44047,44056,44062,44063,44065,44066,44067,44069,44070,44071,44072,44073,44074,44075,44078,44082,44083,44084,null,null,null,null,null,null,44085,44086,44087,44090,44091,44093,44094,44095,44097,44098,44099,44100,44101,44102,44103,44104,44105,44106,44108,44110,44111,44112,44113,44114,44115,44117,null,null,null,null,null,null,44118,44119,44121,44122,44123,44125,44126,44127,44128,44129,44130,44131,44132,44133,44134,44135,44136,44137,44138,44139,44140,44141,44142,44143,44146,44147,44149,44150,44153,44155,44156,44157,44158,44159,44162,44167,44168,44173,44174,44175,44177,44178,44179,44181,44182,44183,44184,44185,44186,44187,44190,44194,44195,44196,44197,44198,44199,44203,44205,44206,44209,44210,44211,44212,44213,44214,44215,44218,44222,44223,44224,44226,44227,44229,44230,44231,44233,44234,44235,44237,44238,44239,44240,44241,44242,44243,44244,44246,44248,44249,44250,44251,44252,44253,44254,44255,44258,44259,44261,44262,44265,44267,44269,44270,44274,44276,44279,44280,44281,44282,44283,44286,44287,44289,44290,44291,44293,44295,44296,44297,44298,44299,44302,44304,44306,44307,44308,44309,44310,44311,44313,44314,44315,44317,44318,44319,44321,44322,44323,44324,44325,44326,44327,44328,44330,44331,44334,44335,44336,44337,44338,44339,null,null,null,null,null,null,44342,44343,44345,44346,44347,44349,44350,44351,44352,44353,44354,44355,44358,44360,44362,44363,44364,44365,44366,44367,44369,44370,44371,44373,44374,44375,null,null,null,null,null,null,44377,44378,44379,44380,44381,44382,44383,44384,44386,44388,44389,44390,44391,44392,44393,44394,44395,44398,44399,44401,44402,44407,44408,44409,44410,44414,44416,44419,44420,44421,44422,44423,44426,44427,44429,44430,44431,44433,44434,44435,44436,44437,44438,44439,44440,44441,44442,44443,44446,44447,44448,44449,44450,44451,44453,44454,44455,44456,44457,44458,44459,44460,44461,44462,44463,44464,44465,44466,44467,44468,44469,44470,44472,44473,44474,44475,44476,44477,44478,44479,44482,44483,44485,44486,44487,44489,44490,44491,44492,44493,44494,44495,44498,44500,44501,44502,44503,44504,44505,44506,44507,44509,44510,44511,44513,44514,44515,44517,44518,44519,44520,44521,44522,44523,44524,44525,44526,44527,44528,44529,44530,44531,44532,44533,44534,44535,44538,44539,44541,44542,44546,44547,44548,44549,44550,44551,44554,44556,44558,44559,44560,44561,44562,44563,44565,44566,44567,44568,44569,44570,44571,44572,null,null,null,null,null,null,44573,44574,44575,44576,44577,44578,44579,44580,44581,44582,44583,44584,44585,44586,44587,44588,44589,44590,44591,44594,44595,44597,44598,44601,44603,44604,null,null,null,null,null,null,44605,44606,44607,44610,44612,44615,44616,44617,44619,44623,44625,44626,44627,44629,44631,44632,44633,44634,44635,44638,44642,44643,44644,44646,44647,44650,44651,44653,44654,44655,44657,44658,44659,44660,44661,44662,44663,44666,44670,44671,44672,44673,44674,44675,44678,44679,44680,44681,44682,44683,44685,44686,44687,44688,44689,44690,44691,44692,44693,44694,44695,44696,44697,44698,44699,44700,44701,44702,44703,44704,44705,44706,44707,44708,44709,44710,44711,44712,44713,44714,44715,44716,44717,44718,44719,44720,44721,44722,44723,44724,44725,44726,44727,44728,44729,44730,44731,44735,44737,44738,44739,44741,44742,44743,44744,44745,44746,44747,44750,44754,44755,44756,44757,44758,44759,44762,44763,44765,44766,44767,44768,44769,44770,44771,44772,44773,44774,44775,44777,44778,44780,44782,44783,44784,44785,44786,44787,44789,44790,44791,44793,44794,44795,44797,44798,44799,44800,44801,44802,44803,44804,44805,null,null,null,null,null,null,44806,44809,44810,44811,44812,44814,44815,44817,44818,44819,44820,44821,44822,44823,44824,44825,44826,44827,44828,44829,44830,44831,44832,44833,44834,44835,null,null,null,null,null,null,44836,44837,44838,44839,44840,44841,44842,44843,44846,44847,44849,44851,44853,44854,44855,44856,44857,44858,44859,44862,44864,44868,44869,44870,44871,44874,44875,44876,44877,44878,44879,44881,44882,44883,44884,44885,44886,44887,44888,44889,44890,44891,44894,44895,44896,44897,44898,44899,44902,44903,44904,44905,44906,44907,44908,44909,44910,44911,44912,44913,44914,44915,44916,44917,44918,44919,44920,44922,44923,44924,44925,44926,44927,44929,44930,44931,44933,44934,44935,44937,44938,44939,44940,44941,44942,44943,44946,44947,44948,44950,44951,44952,44953,44954,44955,44957,44958,44959,44960,44961,44962,44963,44964,44965,44966,44967,44968,44969,44970,44971,44972,44973,44974,44975,44976,44977,44978,44979,44980,44981,44982,44983,44986,44987,44989,44990,44991,44993,44994,44995,44996,44997,44998,45002,45004,45007,45008,45009,45010,45011,45013,45014,45015,45016,45017,45018,45019,45021,45022,45023,45024,45025,null,null,null,null,null,null,45026,45027,45028,45029,45030,45031,45034,45035,45036,45037,45038,45039,45042,45043,45045,45046,45047,45049,45050,45051,45052,45053,45054,45055,45058,45059,null,null,null,null,null,null,45061,45062,45063,45064,45065,45066,45067,45069,45070,45071,45073,45074,45075,45077,45078,45079,45080,45081,45082,45083,45086,45087,45088,45089,45090,45091,45092,45093,45094,45095,45097,45098,45099,45100,45101,45102,45103,45104,45105,45106,45107,45108,45109,45110,45111,45112,45113,45114,45115,45116,45117,45118,45119,45120,45121,45122,45123,45126,45127,45129,45131,45133,45135,45136,45137,45138,45142,45144,45146,45147,45148,45150,45151,45152,45153,45154,45155,45156,45157,45158,45159,45160,45161,45162,45163,45164,45165,45166,45167,45168,45169,45170,45171,45172,45173,45174,45175,45176,45177,45178,45179,45182,45183,45185,45186,45187,45189,45190,45191,45192,45193,45194,45195,45198,45200,45202,45203,45204,45205,45206,45207,45211,45213,45214,45219,45220,45221,45222,45223,45226,45232,45234,45238,45239,45241,45242,45243,45245,45246,45247,45248,45249,45250,45251,45254,45258,45259,45260,45261,45262,45263,45266,null,null,null,null,null,null,45267,45269,45270,45271,45273,45274,45275,45276,45277,45278,45279,45281,45282,45283,45284,45286,45287,45288,45289,45290,45291,45292,45293,45294,45295,45296,null,null,null,null,null,null,45297,45298,45299,45300,45301,45302,45303,45304,45305,45306,45307,45308,45309,45310,45311,45312,45313,45314,45315,45316,45317,45318,45319,45322,45325,45326,45327,45329,45332,45333,45334,45335,45338,45342,45343,45344,45345,45346,45350,45351,45353,45354,45355,45357,45358,45359,45360,45361,45362,45363,45366,45370,45371,45372,45373,45374,45375,45378,45379,45381,45382,45383,45385,45386,45387,45388,45389,45390,45391,45394,45395,45398,45399,45401,45402,45403,45405,45406,45407,45409,45410,45411,45412,45413,45414,45415,45416,45417,45418,45419,45420,45421,45422,45423,45424,45425,45426,45427,45428,45429,45430,45431,45434,45435,45437,45438,45439,45441,45443,45444,45445,45446,45447,45450,45452,45454,45455,45456,45457,45461,45462,45463,45465,45466,45467,45469,45470,45471,45472,45473,45474,45475,45476,45477,45478,45479,45481,45482,45483,45484,45485,45486,45487,45488,45489,45490,45491,45492,45493,45494,45495,45496,null,null,null,null,null,null,45497,45498,45499,45500,45501,45502,45503,45504,45505,45506,45507,45508,45509,45510,45511,45512,45513,45514,45515,45517,45518,45519,45521,45522,45523,45525,null,null,null,null,null,null,45526,45527,45528,45529,45530,45531,45534,45536,45537,45538,45539,45540,45541,45542,45543,45546,45547,45549,45550,45551,45553,45554,45555,45556,45557,45558,45559,45560,45562,45564,45566,45567,45568,45569,45570,45571,45574,45575,45577,45578,45581,45582,45583,45584,45585,45586,45587,45590,45592,45594,45595,45596,45597,45598,45599,45601,45602,45603,45604,45605,45606,45607,45608,45609,45610,45611,45612,45613,45614,45615,45616,45617,45618,45619,45621,45622,45623,45624,45625,45626,45627,45629,45630,45631,45632,45633,45634,45635,45636,45637,45638,45639,45640,45641,45642,45643,45644,45645,45646,45647,45648,45649,45650,45651,45652,45653,45654,45655,45657,45658,45659,45661,45662,45663,45665,45666,45667,45668,45669,45670,45671,45674,45675,45676,45677,45678,45679,45680,45681,45682,45683,45686,45687,45688,45689,45690,45691,45693,45694,45695,45696,45697,45698,45699,45702,45703,45704,45706,45707,45708,45709,45710,null,null,null,null,null,null,45711,45714,45715,45717,45718,45719,45723,45724,45725,45726,45727,45730,45732,45735,45736,45737,45739,45741,45742,45743,45745,45746,45747,45749,45750,45751,null,null,null,null,null,null,45752,45753,45754,45755,45756,45757,45758,45759,45760,45761,45762,45763,45764,45765,45766,45767,45770,45771,45773,45774,45775,45777,45779,45780,45781,45782,45783,45786,45788,45790,45791,45792,45793,45795,45799,45801,45802,45808,45809,45810,45814,45820,45821,45822,45826,45827,45829,45830,45831,45833,45834,45835,45836,45837,45838,45839,45842,45846,45847,45848,45849,45850,45851,45853,45854,45855,45856,45857,45858,45859,45860,45861,45862,45863,45864,45865,45866,45867,45868,45869,45870,45871,45872,45873,45874,45875,45876,45877,45878,45879,45880,45881,45882,45883,45884,45885,45886,45887,45888,45889,45890,45891,45892,45893,45894,45895,45896,45897,45898,45899,45900,45901,45902,45903,45904,45905,45906,45907,45911,45913,45914,45917,45920,45921,45922,45923,45926,45928,45930,45932,45933,45935,45938,45939,45941,45942,45943,45945,45946,45947,45948,45949,45950,45951,45954,45958,45959,45960,45961,45962,45963,45965,null,null,null,null,null,null,45966,45967,45969,45970,45971,45973,45974,45975,45976,45977,45978,45979,45980,45981,45982,45983,45986,45987,45988,45989,45990,45991,45993,45994,45995,45997,null,null,null,null,null,null,45998,45999,46000,46001,46002,46003,46004,46005,46006,46007,46008,46009,46010,46011,46012,46013,46014,46015,46016,46017,46018,46019,46022,46023,46025,46026,46029,46031,46033,46034,46035,46038,46040,46042,46044,46046,46047,46049,46050,46051,46053,46054,46055,46057,46058,46059,46060,46061,46062,46063,46064,46065,46066,46067,46068,46069,46070,46071,46072,46073,46074,46075,46077,46078,46079,46080,46081,46082,46083,46084,46085,46086,46087,46088,46089,46090,46091,46092,46093,46094,46095,46097,46098,46099,46100,46101,46102,46103,46105,46106,46107,46109,46110,46111,46113,46114,46115,46116,46117,46118,46119,46122,46124,46125,46126,46127,46128,46129,46130,46131,46133,46134,46135,46136,46137,46138,46139,46140,46141,46142,46143,46144,46145,46146,46147,46148,46149,46150,46151,46152,46153,46154,46155,46156,46157,46158,46159,46162,46163,46165,46166,46167,46169,46170,46171,46172,46173,46174,46175,46178,46180,46182,null,null,null,null,null,null,46183,46184,46185,46186,46187,46189,46190,46191,46192,46193,46194,46195,46196,46197,46198,46199,46200,46201,46202,46203,46204,46205,46206,46207,46209,46210,null,null,null,null,null,null,46211,46212,46213,46214,46215,46217,46218,46219,46220,46221,46222,46223,46224,46225,46226,46227,46228,46229,46230,46231,46232,46233,46234,46235,46236,46238,46239,46240,46241,46242,46243,46245,46246,46247,46249,46250,46251,46253,46254,46255,46256,46257,46258,46259,46260,46262,46264,46266,46267,46268,46269,46270,46271,46273,46274,46275,46277,46278,46279,46281,46282,46283,46284,46285,46286,46287,46289,46290,46291,46292,46294,46295,46296,46297,46298,46299,46302,46303,46305,46306,46309,46311,46312,46313,46314,46315,46318,46320,46322,46323,46324,46325,46326,46327,46329,46330,46331,46332,46333,46334,46335,46336,46337,46338,46339,46340,46341,46342,46343,46344,46345,46346,46347,46348,46349,46350,46351,46352,46353,46354,46355,46358,46359,46361,46362,46365,46366,46367,46368,46369,46370,46371,46374,46379,46380,46381,46382,46383,46386,46387,46389,46390,46391,46393,46394,46395,46396,46397,46398,46399,46402,46406,null,null,null,null,null,null,46407,46408,46409,46410,46414,46415,46417,46418,46419,46421,46422,46423,46424,46425,46426,46427,46430,46434,46435,46436,46437,46438,46439,46440,46441,46442,null,null,null,null,null,null,46443,46444,46445,46446,46447,46448,46449,46450,46451,46452,46453,46454,46455,46456,46457,46458,46459,46460,46461,46462,46463,46464,46465,46466,46467,46468,46469,46470,46471,46472,46473,46474,46475,46476,46477,46478,46479,46480,46481,46482,46483,46484,46485,46486,46487,46488,46489,46490,46491,46492,46493,46494,46495,46498,46499,46501,46502,46503,46505,46508,46509,46510,46511,46514,46518,46519,46520,46521,46522,46526,46527,46529,46530,46531,46533,46534,46535,46536,46537,46538,46539,46542,46546,46547,46548,46549,46550,46551,46553,46554,46555,46556,46557,46558,46559,46560,46561,46562,46563,46564,46565,46566,46567,46568,46569,46570,46571,46573,46574,46575,46576,46577,46578,46579,46580,46581,46582,46583,46584,46585,46586,46587,46588,46589,46590,46591,46592,46593,46594,46595,46596,46597,46598,46599,46600,46601,46602,46603,46604,46605,46606,46607,46610,46611,46613,46614,46615,46617,46618,46619,46620,46621,null,null,null,null,null,null,46622,46623,46624,46625,46626,46627,46628,46630,46631,46632,46633,46634,46635,46637,46638,46639,46640,46641,46642,46643,46645,46646,46647,46648,46649,46650,null,null,null,null,null,null,46651,46652,46653,46654,46655,46656,46657,46658,46659,46660,46661,46662,46663,46665,46666,46667,46668,46669,46670,46671,46672,46673,46674,46675,46676,46677,46678,46679,46680,46681,46682,46683,46684,46685,46686,46687,46688,46689,46690,46691,46693,46694,46695,46697,46698,46699,46700,46701,46702,46703,46704,46705,46706,46707,46708,46709,46710,46711,46712,46713,46714,46715,46716,46717,46718,46719,46720,46721,46722,46723,46724,46725,46726,46727,46728,46729,46730,46731,46732,46733,46734,46735,46736,46737,46738,46739,46740,46741,46742,46743,46744,46745,46746,46747,46750,46751,46753,46754,46755,46757,46758,46759,46760,46761,46762,46765,46766,46767,46768,46770,46771,46772,46773,46774,46775,46776,46777,46778,46779,46780,46781,46782,46783,46784,46785,46786,46787,46788,46789,46790,46791,46792,46793,46794,46795,46796,46797,46798,46799,46800,46801,46802,46803,46805,46806,46807,46808,46809,46810,46811,46812,46813,null,null,null,null,null,null,46814,46815,46816,46817,46818,46819,46820,46821,46822,46823,46824,46825,46826,46827,46828,46829,46830,46831,46833,46834,46835,46837,46838,46839,46841,46842,null,null,null,null,null,null,46843,46844,46845,46846,46847,46850,46851,46852,46854,46855,46856,46857,46858,46859,46860,46861,46862,46863,46864,46865,46866,46867,46868,46869,46870,46871,46872,46873,46874,46875,46876,46877,46878,46879,46880,46881,46882,46883,46884,46885,46886,46887,46890,46891,46893,46894,46897,46898,46899,46900,46901,46902,46903,46906,46908,46909,46910,46911,46912,46913,46914,46915,46917,46918,46919,46921,46922,46923,46925,46926,46927,46928,46929,46930,46931,46934,46935,46936,46937,46938,46939,46940,46941,46942,46943,46945,46946,46947,46949,46950,46951,46953,46954,46955,46956,46957,46958,46959,46962,46964,46966,46967,46968,46969,46970,46971,46974,46975,46977,46978,46979,46981,46982,46983,46984,46985,46986,46987,46990,46995,46996,46997,47002,47003,47005,47006,47007,47009,47010,47011,47012,47013,47014,47015,47018,47022,47023,47024,47025,47026,47027,47030,47031,47033,47034,47035,47036,47037,47038,47039,47040,47041,null,null,null,null,null,null,47042,47043,47044,47045,47046,47048,47050,47051,47052,47053,47054,47055,47056,47057,47058,47059,47060,47061,47062,47063,47064,47065,47066,47067,47068,47069,null,null,null,null,null,null,47070,47071,47072,47073,47074,47075,47076,47077,47078,47079,47080,47081,47082,47083,47086,47087,47089,47090,47091,47093,47094,47095,47096,47097,47098,47099,47102,47106,47107,47108,47109,47110,47114,47115,47117,47118,47119,47121,47122,47123,47124,47125,47126,47127,47130,47132,47134,47135,47136,47137,47138,47139,47142,47143,47145,47146,47147,47149,47150,47151,47152,47153,47154,47155,47158,47162,47163,47164,47165,47166,47167,47169,47170,47171,47173,47174,47175,47176,47177,47178,47179,47180,47181,47182,47183,47184,47186,47188,47189,47190,47191,47192,47193,47194,47195,47198,47199,47201,47202,47203,47205,47206,47207,47208,47209,47210,47211,47214,47216,47218,47219,47220,47221,47222,47223,47225,47226,47227,47229,47230,47231,47232,47233,47234,47235,47236,47237,47238,47239,47240,47241,47242,47243,47244,47246,47247,47248,47249,47250,47251,47252,47253,47254,47255,47256,47257,47258,47259,47260,47261,47262,47263,null,null,null,null,null,null,47264,47265,47266,47267,47268,47269,47270,47271,47273,47274,47275,47276,47277,47278,47279,47281,47282,47283,47285,47286,47287,47289,47290,47291,47292,47293,null,null,null,null,null,null,47294,47295,47298,47300,47302,47303,47304,47305,47306,47307,47309,47310,47311,47313,47314,47315,47317,47318,47319,47320,47321,47322,47323,47324,47326,47328,47330,47331,47332,47333,47334,47335,47338,47339,47341,47342,47343,47345,47346,47347,47348,47349,47350,47351,47354,47356,47358,47359,47360,47361,47362,47363,47365,47366,47367,47368,47369,47370,47371,47372,47373,47374,47375,47376,47377,47378,47379,47380,47381,47382,47383,47385,47386,47387,47388,47389,47390,47391,47393,47394,47395,47396,47397,47398,47399,47400,47401,47402,47403,47404,47405,47406,47407,47408,47409,47410,47411,47412,47413,47414,47415,47416,47417,47418,47419,47422,47423,47425,47426,47427,47429,47430,47431,47432,47433,47434,47435,47437,47438,47440,47442,47443,47444,47445,47446,47447,47450,47451,47453,47454,47455,47457,47458,47459,47460,47461,47462,47463,47466,47468,47470,47471,47472,47473,47474,47475,47478,47479,47481,47482,47483,47485,null,null,null,null,null,null,47486,47487,47488,47489,47490,47491,47494,47496,47499,47500,47503,47504,47505,47506,47507,47508,47509,47510,47511,47512,47513,47514,47515,47516,47517,47518,null,null,null,null,null,null,47519,47520,47521,47522,47523,47524,47525,47526,47527,47528,47529,47530,47531,47534,47535,47537,47538,47539,47541,47542,47543,47544,47545,47546,47547,47550,47552,47554,47555,47556,47557,47558,47559,47562,47563,47565,47571,47572,47573,47574,47575,47578,47580,47583,47584,47586,47590,47591,47593,47594,47595,47597,47598,47599,47600,47601,47602,47603,47606,47611,47612,47613,47614,47615,47618,47619,47620,47621,47622,47623,47625,47626,47627,47628,47629,47630,47631,47632,47633,47634,47635,47636,47638,47639,47640,47641,47642,47643,47644,47645,47646,47647,47648,47649,47650,47651,47652,47653,47654,47655,47656,47657,47658,47659,47660,47661,47662,47663,47664,47665,47666,47667,47668,47669,47670,47671,47674,47675,47677,47678,47679,47681,47683,47684,47685,47686,47687,47690,47692,47695,47696,47697,47698,47702,47703,47705,47706,47707,47709,47710,47711,47712,47713,47714,47715,47718,47722,47723,47724,47725,47726,47727,null,null,null,null,null,null,47730,47731,47733,47734,47735,47737,47738,47739,47740,47741,47742,47743,47744,47745,47746,47750,47752,47753,47754,47755,47757,47758,47759,47760,47761,47762,null,null,null,null,null,null,47763,47764,47765,47766,47767,47768,47769,47770,47771,47772,47773,47774,47775,47776,47777,47778,47779,47780,47781,47782,47783,47786,47789,47790,47791,47793,47795,47796,47797,47798,47799,47802,47804,47806,47807,47808,47809,47810,47811,47813,47814,47815,47817,47818,47819,47820,47821,47822,47823,47824,47825,47826,47827,47828,47829,47830,47831,47834,47835,47836,47837,47838,47839,47840,47841,47842,47843,47844,47845,47846,47847,47848,47849,47850,47851,47852,47853,47854,47855,47856,47857,47858,47859,47860,47861,47862,47863,47864,47865,47866,47867,47869,47870,47871,47873,47874,47875,47877,47878,47879,47880,47881,47882,47883,47884,47886,47888,47890,47891,47892,47893,47894,47895,47897,47898,47899,47901,47902,47903,47905,47906,47907,47908,47909,47910,47911,47912,47914,47916,47917,47918,47919,47920,47921,47922,47923,47927,47929,47930,47935,47936,47937,47938,47939,47942,47944,47946,47947,47948,47950,47953,47954,null,null,null,null,null,null,47955,47957,47958,47959,47961,47962,47963,47964,47965,47966,47967,47968,47970,47972,47973,47974,47975,47976,47977,47978,47979,47981,47982,47983,47984,47985,null,null,null,null,null,null,47986,47987,47988,47989,47990,47991,47992,47993,47994,47995,47996,47997,47998,47999,48000,48001,48002,48003,48004,48005,48006,48007,48009,48010,48011,48013,48014,48015,48017,48018,48019,48020,48021,48022,48023,48024,48025,48026,48027,48028,48029,48030,48031,48032,48033,48034,48035,48037,48038,48039,48041,48042,48043,48045,48046,48047,48048,48049,48050,48051,48053,48054,48056,48057,48058,48059,48060,48061,48062,48063,48065,48066,48067,48069,48070,48071,48073,48074,48075,48076,48077,48078,48079,48081,48082,48084,48085,48086,48087,48088,48089,48090,48091,48092,48093,48094,48095,48096,48097,48098,48099,48100,48101,48102,48103,48104,48105,48106,48107,48108,48109,48110,48111,48112,48113,48114,48115,48116,48117,48118,48119,48122,48123,48125,48126,48129,48131,48132,48133,48134,48135,48138,48142,48144,48146,48147,48153,48154,48160,48161,48162,48163,48166,48168,48170,48171,48172,48174,48175,48178,48179,48181,null,null,null,null,null,null,48182,48183,48185,48186,48187,48188,48189,48190,48191,48194,48198,48199,48200,48202,48203,48206,48207,48209,48210,48211,48212,48213,48214,48215,48216,48217,null,null,null,null,null,null,48218,48219,48220,48222,48223,48224,48225,48226,48227,48228,48229,48230,48231,48232,48233,48234,48235,48236,48237,48238,48239,48240,48241,48242,48243,48244,48245,48246,48247,48248,48249,48250,48251,48252,48253,48254,48255,48256,48257,48258,48259,48262,48263,48265,48266,48269,48271,48272,48273,48274,48275,48278,48280,48283,48284,48285,48286,48287,48290,48291,48293,48294,48297,48298,48299,48300,48301,48302,48303,48306,48310,48311,48312,48313,48314,48315,48318,48319,48321,48322,48323,48325,48326,48327,48328,48329,48330,48331,48332,48334,48338,48339,48340,48342,48343,48345,48346,48347,48349,48350,48351,48352,48353,48354,48355,48356,48357,48358,48359,48360,48361,48362,48363,48364,48365,48366,48367,48368,48369,48370,48371,48375,48377,48378,48379,48381,48382,48383,48384,48385,48386,48387,48390,48392,48394,48395,48396,48397,48398,48399,48401,48402,48403,48405,48406,48407,48408,48409,48410,48411,48412,48413,null,null,null,null,null,null,48414,48415,48416,48417,48418,48419,48421,48422,48423,48424,48425,48426,48427,48429,48430,48431,48432,48433,48434,48435,48436,48437,48438,48439,48440,48441,null,null,null,null,null,null,48442,48443,48444,48445,48446,48447,48449,48450,48451,48452,48453,48454,48455,48458,48459,48461,48462,48463,48465,48466,48467,48468,48469,48470,48471,48474,48475,48476,48477,48478,48479,48480,48481,48482,48483,48485,48486,48487,48489,48490,48491,48492,48493,48494,48495,48496,48497,48498,48499,48500,48501,48502,48503,48504,48505,48506,48507,48508,48509,48510,48511,48514,48515,48517,48518,48523,48524,48525,48526,48527,48530,48532,48534,48535,48536,48539,48541,48542,48543,48544,48545,48546,48547,48549,48550,48551,48552,48553,48554,48555,48556,48557,48558,48559,48561,48562,48563,48564,48565,48566,48567,48569,48570,48571,48572,48573,48574,48575,48576,48577,48578,48579,48580,48581,48582,48583,48584,48585,48586,48587,48588,48589,48590,48591,48592,48593,48594,48595,48598,48599,48601,48602,48603,48605,48606,48607,48608,48609,48610,48611,48612,48613,48614,48615,48616,48618,48619,48620,48621,48622,48623,48625,null,null,null,null,null,null,48626,48627,48629,48630,48631,48633,48634,48635,48636,48637,48638,48639,48641,48642,48644,48646,48647,48648,48649,48650,48651,48654,48655,48657,48658,48659,null,null,null,null,null,null,48661,48662,48663,48664,48665,48666,48667,48670,48672,48673,48674,48675,48676,48677,48678,48679,48680,48681,48682,48683,48684,48685,48686,48687,48688,48689,48690,48691,48692,48693,48694,48695,48696,48697,48698,48699,48700,48701,48702,48703,48704,48705,48706,48707,48710,48711,48713,48714,48715,48717,48719,48720,48721,48722,48723,48726,48728,48732,48733,48734,48735,48738,48739,48741,48742,48743,48745,48747,48748,48749,48750,48751,48754,48758,48759,48760,48761,48762,48766,48767,48769,48770,48771,48773,48774,48775,48776,48777,48778,48779,48782,48786,48787,48788,48789,48790,48791,48794,48795,48796,48797,48798,48799,48800,48801,48802,48803,48804,48805,48806,48807,48809,48810,48811,48812,48813,48814,48815,48816,48817,48818,48819,48820,48821,48822,48823,48824,48825,48826,48827,48828,48829,48830,48831,48832,48833,48834,48835,48836,48837,48838,48839,48840,48841,48842,48843,48844,48845,48846,48847,48850,48851,null,null,null,null,null,null,48853,48854,48857,48858,48859,48860,48861,48862,48863,48865,48866,48870,48871,48872,48873,48874,48875,48877,48878,48879,48880,48881,48882,48883,48884,48885,null,null,null,null,null,null,48886,48887,48888,48889,48890,48891,48892,48893,48894,48895,48896,48898,48899,48900,48901,48902,48903,48906,48907,48908,48909,48910,48911,48912,48913,48914,48915,48916,48917,48918,48919,48922,48926,48927,48928,48929,48930,48931,48932,48933,48934,48935,48936,48937,48938,48939,48940,48941,48942,48943,48944,48945,48946,48947,48948,48949,48950,48951,48952,48953,48954,48955,48956,48957,48958,48959,48962,48963,48965,48966,48967,48969,48970,48971,48972,48973,48974,48975,48978,48979,48980,48982,48983,48984,48985,48986,48987,48988,48989,48990,48991,48992,48993,48994,48995,48996,48997,48998,48999,49000,49001,49002,49003,49004,49005,49006,49007,49008,49009,49010,49011,49012,49013,49014,49015,49016,49017,49018,49019,49020,49021,49022,49023,49024,49025,49026,49027,49028,49029,49030,49031,49032,49033,49034,49035,49036,49037,49038,49039,49040,49041,49042,49043,49045,49046,49047,49048,49049,49050,49051,49052,49053,null,null,null,null,null,null,49054,49055,49056,49057,49058,49059,49060,49061,49062,49063,49064,49065,49066,49067,49068,49069,49070,49071,49073,49074,49075,49076,49077,49078,49079,49080,null,null,null,null,null,null,49081,49082,49083,49084,49085,49086,49087,49088,49089,49090,49091,49092,49094,49095,49096,49097,49098,49099,49102,49103,49105,49106,49107,49109,49110,49111,49112,49113,49114,49115,49117,49118,49120,49122,49123,49124,49125,49126,49127,49128,49129,49130,49131,49132,49133,49134,49135,49136,49137,49138,49139,49140,49141,49142,49143,49144,49145,49146,49147,49148,49149,49150,49151,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49162,49163,49164,49165,49166,49167,49168,49169,49170,49171,49172,49173,49174,49175,49176,49177,49178,49179,49180,49181,49182,49183,49184,49185,49186,49187,49188,49189,49190,49191,49192,49193,49194,49195,49196,49197,49198,49199,49200,49201,49202,49203,49204,49205,49206,49207,49208,49209,49210,49211,49213,49214,49215,49216,49217,49218,49219,49220,49221,49222,49223,49224,49225,49226,49227,49228,49229,49230,49231,49232,49234,49235,49236,49237,49238,49239,49241,49242,49243,null,null,null,null,null,null,49245,49246,49247,49249,49250,49251,49252,49253,49254,49255,49258,49259,49260,49261,49262,49263,49264,49265,49266,49267,49268,49269,49270,49271,49272,49273,null,null,null,null,null,null,49274,49275,49276,49277,49278,49279,49280,49281,49282,49283,49284,49285,49286,49287,49288,49289,49290,49291,49292,49293,49294,49295,49298,49299,49301,49302,49303,49305,49306,49307,49308,49309,49310,49311,49314,49316,49318,49319,49320,49321,49322,49323,49326,49329,49330,49335,49336,49337,49338,49339,49342,49346,49347,49348,49350,49351,49354,49355,49357,49358,49359,49361,49362,49363,49364,49365,49366,49367,49370,49374,49375,49376,49377,49378,49379,49382,49383,49385,49386,49387,49389,49390,49391,49392,49393,49394,49395,49398,49400,49402,49403,49404,49405,49406,49407,49409,49410,49411,49413,49414,49415,49417,49418,49419,49420,49421,49422,49423,49425,49426,49427,49428,49430,49431,49432,49433,49434,49435,49441,49442,49445,49448,49449,49450,49451,49454,49458,49459,49460,49461,49463,49466,49467,49469,49470,49471,49473,49474,49475,49476,49477,49478,49479,49482,49486,49487,49488,49489,49490,49491,49494,49495,null,null,null,null,null,null,49497,49498,49499,49501,49502,49503,49504,49505,49506,49507,49510,49514,49515,49516,49517,49518,49519,49521,49522,49523,49525,49526,49527,49529,49530,49531,null,null,null,null,null,null,49532,49533,49534,49535,49536,49537,49538,49539,49540,49542,49543,49544,49545,49546,49547,49551,49553,49554,49555,49557,49559,49560,49561,49562,49563,49566,49568,49570,49571,49572,49574,49575,49578,49579,49581,49582,49583,49585,49586,49587,49588,49589,49590,49591,49592,49593,49594,49595,49596,49598,49599,49600,49601,49602,49603,49605,49606,49607,49609,49610,49611,49613,49614,49615,49616,49617,49618,49619,49621,49622,49625,49626,49627,49628,49629,49630,49631,49633,49634,49635,49637,49638,49639,49641,49642,49643,49644,49645,49646,49647,49650,49652,49653,49654,49655,49656,49657,49658,49659,49662,49663,49665,49666,49667,49669,49670,49671,49672,49673,49674,49675,49678,49680,49682,49683,49684,49685,49686,49687,49690,49691,49693,49694,49697,49698,49699,49700,49701,49702,49703,49706,49708,49710,49712,49715,49717,49718,49719,49720,49721,49722,49723,49724,49725,49726,49727,49728,49729,49730,49731,49732,49733,null,null,null,null,null,null,49734,49735,49737,49738,49739,49740,49741,49742,49743,49746,49747,49749,49750,49751,49753,49754,49755,49756,49757,49758,49759,49761,49762,49763,49764,49766,null,null,null,null,null,null,49767,49768,49769,49770,49771,49774,49775,49777,49778,49779,49781,49782,49783,49784,49785,49786,49787,49790,49792,49794,49795,49796,49797,49798,49799,49802,49803,49804,49805,49806,49807,49809,49810,49811,49812,49813,49814,49815,49817,49818,49820,49822,49823,49824,49825,49826,49827,49830,49831,49833,49834,49835,49838,49839,49840,49841,49842,49843,49846,49848,49850,49851,49852,49853,49854,49855,49856,49857,49858,49859,49860,49861,49862,49863,49864,49865,49866,49867,49868,49869,49870,49871,49872,49873,49874,49875,49876,49877,49878,49879,49880,49881,49882,49883,49886,49887,49889,49890,49893,49894,49895,49896,49897,49898,49902,49904,49906,49907,49908,49909,49911,49914,49917,49918,49919,49921,49922,49923,49924,49925,49926,49927,49930,49931,49934,49935,49936,49937,49938,49942,49943,49945,49946,49947,49949,49950,49951,49952,49953,49954,49955,49958,49959,49962,49963,49964,49965,49966,49967,49968,49969,49970,null,null,null,null,null,null,49971,49972,49973,49974,49975,49976,49977,49978,49979,49980,49981,49982,49983,49984,49985,49986,49987,49988,49990,49991,49992,49993,49994,49995,49996,49997,null,null,null,null,null,null,49998,49999,50000,50001,50002,50003,50004,50005,50006,50007,50008,50009,50010,50011,50012,50013,50014,50015,50016,50017,50018,50019,50020,50021,50022,50023,50026,50027,50029,50030,50031,50033,50035,50036,50037,50038,50039,50042,50043,50046,50047,50048,50049,50050,50051,50053,50054,50055,50057,50058,50059,50061,50062,50063,50064,50065,50066,50067,50068,50069,50070,50071,50072,50073,50074,50075,50076,50077,50078,50079,50080,50081,50082,50083,50084,50085,50086,50087,50088,50089,50090,50091,50092,50093,50094,50095,50096,50097,50098,50099,50100,50101,50102,50103,50104,50105,50106,50107,50108,50109,50110,50111,50113,50114,50115,50116,50117,50118,50119,50120,50121,50122,50123,50124,50125,50126,50127,50128,50129,50130,50131,50132,50133,50134,50135,50138,50139,50141,50142,50145,50147,50148,50149,50150,50151,50154,50155,50156,50158,50159,50160,50161,50162,50163,50166,50167,50169,50170,50171,50172,50173,50174,null,null,null,null,null,null,50175,50176,50177,50178,50179,50180,50181,50182,50183,50185,50186,50187,50188,50189,50190,50191,50193,50194,50195,50196,50197,50198,50199,50200,50201,50202,null,null,null,null,null,null,50203,50204,50205,50206,50207,50208,50209,50210,50211,50213,50214,50215,50216,50217,50218,50219,50221,50222,50223,50225,50226,50227,50229,50230,50231,50232,50233,50234,50235,50238,50239,50240,50241,50242,50243,50244,50245,50246,50247,50249,50250,50251,50252,50253,50254,50255,50256,50257,50258,50259,50260,50261,50262,50263,50264,50265,50266,50267,50268,50269,50270,50271,50272,50273,50274,50275,50278,50279,50281,50282,50283,50285,50286,50287,50288,50289,50290,50291,50294,50295,50296,50298,50299,50300,50301,50302,50303,50305,50306,50307,50308,50309,50310,50311,50312,50313,50314,50315,50316,50317,50318,50319,50320,50321,50322,50323,50325,50326,50327,50328,50329,50330,50331,50333,50334,50335,50336,50337,50338,50339,50340,50341,50342,50343,50344,50345,50346,50347,50348,50349,50350,50351,50352,50353,50354,50355,50356,50357,50358,50359,50361,50362,50363,50365,50366,50367,50368,50369,50370,50371,50372,50373,null,null,null,null,null,null,50374,50375,50376,50377,50378,50379,50380,50381,50382,50383,50384,50385,50386,50387,50388,50389,50390,50391,50392,50393,50394,50395,50396,50397,50398,50399,null,null,null,null,null,null,50400,50401,50402,50403,50404,50405,50406,50407,50408,50410,50411,50412,50413,50414,50415,50418,50419,50421,50422,50423,50425,50427,50428,50429,50430,50434,50435,50436,50437,50438,50439,50440,50441,50442,50443,50445,50446,50447,50449,50450,50451,50453,50454,50455,50456,50457,50458,50459,50461,50462,50463,50464,50465,50466,50467,50468,50469,50470,50471,50474,50475,50477,50478,50479,50481,50482,50483,50484,50485,50486,50487,50490,50492,50494,50495,50496,50497,50498,50499,50502,50503,50507,50511,50512,50513,50514,50518,50522,50523,50524,50527,50530,50531,50533,50534,50535,50537,50538,50539,50540,50541,50542,50543,50546,50550,50551,50552,50553,50554,50555,50558,50559,50561,50562,50563,50565,50566,50568,50569,50570,50571,50574,50576,50578,50579,50580,50582,50585,50586,50587,50589,50590,50591,50593,50594,50595,50596,50597,50598,50599,50600,50602,50603,50604,50605,50606,50607,50608,50609,50610,50611,50614,null,null,null,null,null,null,50615,50618,50623,50624,50625,50626,50627,50635,50637,50639,50642,50643,50645,50646,50647,50649,50650,50651,50652,50653,50654,50655,50658,50660,50662,50663,null,null,null,null,null,null,50664,50665,50666,50667,50671,50673,50674,50675,50677,50680,50681,50682,50683,50690,50691,50692,50697,50698,50699,50701,50702,50703,50705,50706,50707,50708,50709,50710,50711,50714,50717,50718,50719,50720,50721,50722,50723,50726,50727,50729,50730,50731,50735,50737,50738,50742,50744,50746,50748,50749,50750,50751,50754,50755,50757,50758,50759,50761,50762,50763,50764,50765,50766,50767,50770,50774,50775,50776,50777,50778,50779,50782,50783,50785,50786,50787,50788,50789,50790,50791,50792,50793,50794,50795,50797,50798,50800,50802,50803,50804,50805,50806,50807,50810,50811,50813,50814,50815,50817,50818,50819,50820,50821,50822,50823,50826,50828,50830,50831,50832,50833,50834,50835,50838,50839,50841,50842,50843,50845,50846,50847,50848,50849,50850,50851,50854,50856,50858,50859,50860,50861,50862,50863,50866,50867,50869,50870,50871,50875,50876,50877,50878,50879,50882,50884,50886,50887,50888,50889,50890,50891,50894,null,null,null,null,null,null,50895,50897,50898,50899,50901,50902,50903,50904,50905,50906,50907,50910,50911,50914,50915,50916,50917,50918,50919,50922,50923,50925,50926,50927,50929,50930,null,null,null,null,null,null,50931,50932,50933,50934,50935,50938,50939,50940,50942,50943,50944,50945,50946,50947,50950,50951,50953,50954,50955,50957,50958,50959,50960,50961,50962,50963,50966,50968,50970,50971,50972,50973,50974,50975,50978,50979,50981,50982,50983,50985,50986,50987,50988,50989,50990,50991,50994,50996,50998,51000,51001,51002,51003,51006,51007,51009,51010,51011,51013,51014,51015,51016,51017,51019,51022,51024,51033,51034,51035,51037,51038,51039,51041,51042,51043,51044,51045,51046,51047,51049,51050,51052,51053,51054,51055,51056,51057,51058,51059,51062,51063,51065,51066,51067,51071,51072,51073,51074,51078,51083,51084,51085,51087,51090,51091,51093,51097,51099,51100,51101,51102,51103,51106,51111,51112,51113,51114,51115,51118,51119,51121,51122,51123,51125,51126,51127,51128,51129,51130,51131,51134,51138,51139,51140,51141,51142,51143,51146,51147,51149,51151,51153,51154,51155,51156,51157,51158,51159,51161,51162,51163,51164,null,null,null,null,null,null,51166,51167,51168,51169,51170,51171,51173,51174,51175,51177,51178,51179,51181,51182,51183,51184,51185,51186,51187,51188,51189,51190,51191,51192,51193,51194,null,null,null,null,null,null,51195,51196,51197,51198,51199,51202,51203,51205,51206,51207,51209,51211,51212,51213,51214,51215,51218,51220,51223,51224,51225,51226,51227,51230,51231,51233,51234,51235,51237,51238,51239,51240,51241,51242,51243,51246,51248,51250,51251,51252,51253,51254,51255,51257,51258,51259,51261,51262,51263,51265,51266,51267,51268,51269,51270,51271,51274,51275,51278,51279,51280,51281,51282,51283,51285,51286,51287,51288,51289,51290,51291,51292,51293,51294,51295,51296,51297,51298,51299,51300,51301,51302,51303,51304,51305,51306,51307,51308,51309,51310,51311,51314,51315,51317,51318,51319,51321,51323,51324,51325,51326,51327,51330,51332,51336,51337,51338,51342,51343,51344,51345,51346,51347,51349,51350,51351,51352,51353,51354,51355,51356,51358,51360,51362,51363,51364,51365,51366,51367,51369,51370,51371,51372,51373,51374,51375,51376,51377,51378,51379,51380,51381,51382,51383,51384,51385,51386,51387,51390,51391,51392,51393,null,null,null,null,null,null,51394,51395,51397,51398,51399,51401,51402,51403,51405,51406,51407,51408,51409,51410,51411,51414,51416,51418,51419,51420,51421,51422,51423,51426,51427,51429,null,null,null,null,null,null,51430,51431,51432,51433,51434,51435,51436,51437,51438,51439,51440,51441,51442,51443,51444,51446,51447,51448,51449,51450,51451,51454,51455,51457,51458,51459,51463,51464,51465,51466,51467,51470,12288,12289,12290,183,8229,8230,168,12291,173,8213,8741,65340,8764,8216,8217,8220,8221,12308,12309,12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,177,215,247,8800,8804,8805,8734,8756,176,8242,8243,8451,8491,65504,65505,65509,9794,9792,8736,8869,8978,8706,8711,8801,8786,167,8251,9734,9733,9675,9679,9678,9671,9670,9633,9632,9651,9650,9661,9660,8594,8592,8593,8595,8596,12307,8810,8811,8730,8765,8733,8757,8747,8748,8712,8715,8838,8839,8834,8835,8746,8745,8743,8744,65506,51472,51474,51475,51476,51477,51478,51479,51481,51482,51483,51484,51485,51486,51487,51488,51489,51490,51491,51492,51493,51494,51495,51496,51497,51498,51499,null,null,null,null,null,null,51501,51502,51503,51504,51505,51506,51507,51509,51510,51511,51512,51513,51514,51515,51516,51517,51518,51519,51520,51521,51522,51523,51524,51525,51526,51527,null,null,null,null,null,null,51528,51529,51530,51531,51532,51533,51534,51535,51538,51539,51541,51542,51543,51545,51546,51547,51548,51549,51550,51551,51554,51556,51557,51558,51559,51560,51561,51562,51563,51565,51566,51567,8658,8660,8704,8707,180,65374,711,728,733,730,729,184,731,161,191,720,8750,8721,8719,164,8457,8240,9665,9664,9655,9654,9828,9824,9825,9829,9831,9827,8857,9672,9635,9680,9681,9618,9636,9637,9640,9639,9638,9641,9832,9743,9742,9756,9758,182,8224,8225,8597,8599,8601,8598,8600,9837,9833,9834,9836,12927,12828,8470,13255,8482,13250,13272,8481,8364,174,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,51569,51570,51571,51573,51574,51575,51576,51577,51578,51579,51581,51582,51583,51584,51585,51586,51587,51588,51589,51590,51591,51594,51595,51597,51598,51599,null,null,null,null,null,null,51601,51602,51603,51604,51605,51606,51607,51610,51612,51614,51615,51616,51617,51618,51619,51620,51621,51622,51623,51624,51625,51626,51627,51628,51629,51630,null,null,null,null,null,null,51631,51632,51633,51634,51635,51636,51637,51638,51639,51640,51641,51642,51643,51644,51645,51646,51647,51650,51651,51653,51654,51657,51659,51660,51661,51662,51663,51666,51668,51671,51672,51675,65281,65282,65283,65284,65285,65286,65287,65288,65289,65290,65291,65292,65293,65294,65295,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,65306,65307,65308,65309,65310,65311,65312,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65339,65510,65341,65342,65343,65344,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,65371,65372,65373,65507,51678,51679,51681,51683,51685,51686,51688,51689,51690,51691,51694,51698,51699,51700,51701,51702,51703,51706,51707,51709,51710,51711,51713,51714,51715,51716,null,null,null,null,null,null,51717,51718,51719,51722,51726,51727,51728,51729,51730,51731,51733,51734,51735,51737,51738,51739,51740,51741,51742,51743,51744,51745,51746,51747,51748,51749,null,null,null,null,null,null,51750,51751,51752,51754,51755,51756,51757,51758,51759,51760,51761,51762,51763,51764,51765,51766,51767,51768,51769,51770,51771,51772,51773,51774,51775,51776,51777,51778,51779,51780,51781,51782,12593,12594,12595,12596,12597,12598,12599,12600,12601,12602,12603,12604,12605,12606,12607,12608,12609,12610,12611,12612,12613,12614,12615,12616,12617,12618,12619,12620,12621,12622,12623,12624,12625,12626,12627,12628,12629,12630,12631,12632,12633,12634,12635,12636,12637,12638,12639,12640,12641,12642,12643,12644,12645,12646,12647,12648,12649,12650,12651,12652,12653,12654,12655,12656,12657,12658,12659,12660,12661,12662,12663,12664,12665,12666,12667,12668,12669,12670,12671,12672,12673,12674,12675,12676,12677,12678,12679,12680,12681,12682,12683,12684,12685,12686,51783,51784,51785,51786,51787,51790,51791,51793,51794,51795,51797,51798,51799,51800,51801,51802,51803,51806,51810,51811,51812,51813,51814,51815,51817,51818,null,null,null,null,null,null,51819,51820,51821,51822,51823,51824,51825,51826,51827,51828,51829,51830,51831,51832,51833,51834,51835,51836,51838,51839,51840,51841,51842,51843,51845,51846,null,null,null,null,null,null,51847,51848,51849,51850,51851,51852,51853,51854,51855,51856,51857,51858,51859,51860,51861,51862,51863,51865,51866,51867,51868,51869,51870,51871,51872,51873,51874,51875,51876,51877,51878,51879,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,null,null,null,null,null,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,null,null,null,null,null,null,null,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,null,null,null,null,null,null,null,null,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,null,null,null,null,null,null,51880,51881,51882,51883,51884,51885,51886,51887,51888,51889,51890,51891,51892,51893,51894,51895,51896,51897,51898,51899,51902,51903,51905,51906,51907,51909,null,null,null,null,null,null,51910,51911,51912,51913,51914,51915,51918,51920,51922,51924,51925,51926,51927,51930,51931,51932,51933,51934,51935,51937,51938,51939,51940,51941,51942,51943,null,null,null,null,null,null,51944,51945,51946,51947,51949,51950,51951,51952,51953,51954,51955,51957,51958,51959,51960,51961,51962,51963,51964,51965,51966,51967,51968,51969,51970,51971,51972,51973,51974,51975,51977,51978,9472,9474,9484,9488,9496,9492,9500,9516,9508,9524,9532,9473,9475,9487,9491,9499,9495,9507,9523,9515,9531,9547,9504,9519,9512,9527,9535,9501,9520,9509,9528,9538,9490,9489,9498,9497,9494,9493,9486,9485,9502,9503,9505,9506,9510,9511,9513,9514,9517,9518,9521,9522,9525,9526,9529,9530,9533,9534,9536,9537,9539,9540,9541,9542,9543,9544,9545,9546,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,51979,51980,51981,51982,51983,51985,51986,51987,51989,51990,51991,51993,51994,51995,51996,51997,51998,51999,52002,52003,52004,52005,52006,52007,52008,52009,null,null,null,null,null,null,52010,52011,52012,52013,52014,52015,52016,52017,52018,52019,52020,52021,52022,52023,52024,52025,52026,52027,52028,52029,52030,52031,52032,52034,52035,52036,null,null,null,null,null,null,52037,52038,52039,52042,52043,52045,52046,52047,52049,52050,52051,52052,52053,52054,52055,52058,52059,52060,52062,52063,52064,52065,52066,52067,52069,52070,52071,52072,52073,52074,52075,52076,13205,13206,13207,8467,13208,13252,13219,13220,13221,13222,13209,13210,13211,13212,13213,13214,13215,13216,13217,13218,13258,13197,13198,13199,13263,13192,13193,13256,13223,13224,13232,13233,13234,13235,13236,13237,13238,13239,13240,13241,13184,13185,13186,13187,13188,13242,13243,13244,13245,13246,13247,13200,13201,13202,13203,13204,8486,13248,13249,13194,13195,13196,13270,13253,13229,13230,13231,13275,13225,13226,13227,13228,13277,13264,13267,13251,13257,13276,13254,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52077,52078,52079,52080,52081,52082,52083,52084,52085,52086,52087,52090,52091,52092,52093,52094,52095,52096,52097,52098,52099,52100,52101,52102,52103,52104,null,null,null,null,null,null,52105,52106,52107,52108,52109,52110,52111,52112,52113,52114,52115,52116,52117,52118,52119,52120,52121,52122,52123,52125,52126,52127,52128,52129,52130,52131,null,null,null,null,null,null,52132,52133,52134,52135,52136,52137,52138,52139,52140,52141,52142,52143,52144,52145,52146,52147,52148,52149,52150,52151,52153,52154,52155,52156,52157,52158,52159,52160,52161,52162,52163,52164,198,208,170,294,null,306,null,319,321,216,338,186,222,358,330,null,12896,12897,12898,12899,12900,12901,12902,12903,12904,12905,12906,12907,12908,12909,12910,12911,12912,12913,12914,12915,12916,12917,12918,12919,12920,12921,12922,12923,9424,9425,9426,9427,9428,9429,9430,9431,9432,9433,9434,9435,9436,9437,9438,9439,9440,9441,9442,9443,9444,9445,9446,9447,9448,9449,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9322,9323,9324,9325,9326,189,8531,8532,188,190,8539,8540,8541,8542,52165,52166,52167,52168,52169,52170,52171,52172,52173,52174,52175,52176,52177,52178,52179,52181,52182,52183,52184,52185,52186,52187,52188,52189,52190,52191,null,null,null,null,null,null,52192,52193,52194,52195,52197,52198,52200,52202,52203,52204,52205,52206,52207,52208,52209,52210,52211,52212,52213,52214,52215,52216,52217,52218,52219,52220,null,null,null,null,null,null,52221,52222,52223,52224,52225,52226,52227,52228,52229,52230,52231,52232,52233,52234,52235,52238,52239,52241,52242,52243,52245,52246,52247,52248,52249,52250,52251,52254,52255,52256,52259,52260,230,273,240,295,305,307,312,320,322,248,339,223,254,359,331,329,12800,12801,12802,12803,12804,12805,12806,12807,12808,12809,12810,12811,12812,12813,12814,12815,12816,12817,12818,12819,12820,12821,12822,12823,12824,12825,12826,12827,9372,9373,9374,9375,9376,9377,9378,9379,9380,9381,9382,9383,9384,9385,9386,9387,9388,9389,9390,9391,9392,9393,9394,9395,9396,9397,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,9342,9343,9344,9345,9346,185,178,179,8308,8319,8321,8322,8323,8324,52261,52262,52266,52267,52269,52271,52273,52274,52275,52276,52277,52278,52279,52282,52287,52288,52289,52290,52291,52294,52295,52297,52298,52299,52301,52302,null,null,null,null,null,null,52303,52304,52305,52306,52307,52310,52314,52315,52316,52317,52318,52319,52321,52322,52323,52325,52327,52329,52330,52331,52332,52333,52334,52335,52337,52338,null,null,null,null,null,null,52339,52340,52342,52343,52344,52345,52346,52347,52348,52349,52350,52351,52352,52353,52354,52355,52356,52357,52358,52359,52360,52361,52362,52363,52364,52365,52366,52367,52368,52369,52370,52371,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,null,null,null,null,null,null,null,null,null,null,null,52372,52373,52374,52375,52378,52379,52381,52382,52383,52385,52386,52387,52388,52389,52390,52391,52394,52398,52399,52400,52401,52402,52403,52406,52407,52409,null,null,null,null,null,null,52410,52411,52413,52414,52415,52416,52417,52418,52419,52422,52424,52426,52427,52428,52429,52430,52431,52433,52434,52435,52437,52438,52439,52440,52441,52442,null,null,null,null,null,null,52443,52444,52445,52446,52447,52448,52449,52450,52451,52453,52454,52455,52456,52457,52458,52459,52461,52462,52463,52465,52466,52467,52468,52469,52470,52471,52472,52473,52474,52475,52476,52477,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,null,null,null,null,null,null,null,null,52478,52479,52480,52482,52483,52484,52485,52486,52487,52490,52491,52493,52494,52495,52497,52498,52499,52500,52501,52502,52503,52506,52508,52510,52511,52512,null,null,null,null,null,null,52513,52514,52515,52517,52518,52519,52521,52522,52523,52525,52526,52527,52528,52529,52530,52531,52532,52533,52534,52535,52536,52538,52539,52540,52541,52542,null,null,null,null,null,null,52543,52544,52545,52546,52547,52548,52549,52550,52551,52552,52553,52554,52555,52556,52557,52558,52559,52560,52561,52562,52563,52564,52565,52566,52567,52568,52569,52570,52571,52573,52574,52575,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,null,null,null,null,null,null,null,null,null,null,null,null,null,52577,52578,52579,52581,52582,52583,52584,52585,52586,52587,52590,52592,52594,52595,52596,52597,52598,52599,52601,52602,52603,52604,52605,52606,52607,52608,null,null,null,null,null,null,52609,52610,52611,52612,52613,52614,52615,52617,52618,52619,52620,52621,52622,52623,52624,52625,52626,52627,52630,52631,52633,52634,52635,52637,52638,52639,null,null,null,null,null,null,52640,52641,52642,52643,52646,52648,52650,52651,52652,52653,52654,52655,52657,52658,52659,52660,52661,52662,52663,52664,52665,52666,52667,52668,52669,52670,52671,52672,52673,52674,52675,52677,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52678,52679,52680,52681,52682,52683,52685,52686,52687,52689,52690,52691,52692,52693,52694,52695,52696,52697,52698,52699,52700,52701,52702,52703,52704,52705,null,null,null,null,null,null,52706,52707,52708,52709,52710,52711,52713,52714,52715,52717,52718,52719,52721,52722,52723,52724,52725,52726,52727,52730,52732,52734,52735,52736,52737,52738,null,null,null,null,null,null,52739,52741,52742,52743,52745,52746,52747,52749,52750,52751,52752,52753,52754,52755,52757,52758,52759,52760,52762,52763,52764,52765,52766,52767,52770,52771,52773,52774,52775,52777,52778,52779,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52780,52781,52782,52783,52786,52788,52790,52791,52792,52793,52794,52795,52796,52797,52798,52799,52800,52801,52802,52803,52804,52805,52806,52807,52808,52809,null,null,null,null,null,null,52810,52811,52812,52813,52814,52815,52816,52817,52818,52819,52820,52821,52822,52823,52826,52827,52829,52830,52834,52835,52836,52837,52838,52839,52842,52844,null,null,null,null,null,null,52846,52847,52848,52849,52850,52851,52854,52855,52857,52858,52859,52861,52862,52863,52864,52865,52866,52867,52870,52872,52874,52875,52876,52877,52878,52879,52882,52883,52885,52886,52887,52889,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52890,52891,52892,52893,52894,52895,52898,52902,52903,52904,52905,52906,52907,52910,52911,52912,52913,52914,52915,52916,52917,52918,52919,52920,52921,52922,null,null,null,null,null,null,52923,52924,52925,52926,52927,52928,52930,52931,52932,52933,52934,52935,52936,52937,52938,52939,52940,52941,52942,52943,52944,52945,52946,52947,52948,52949,null,null,null,null,null,null,52950,52951,52952,52953,52954,52955,52956,52957,52958,52959,52960,52961,52962,52963,52966,52967,52969,52970,52973,52974,52975,52976,52977,52978,52979,52982,52986,52987,52988,52989,52990,52991,44032,44033,44036,44039,44040,44041,44042,44048,44049,44050,44051,44052,44053,44054,44055,44057,44058,44059,44060,44061,44064,44068,44076,44077,44079,44080,44081,44088,44089,44092,44096,44107,44109,44116,44120,44124,44144,44145,44148,44151,44152,44154,44160,44161,44163,44164,44165,44166,44169,44170,44171,44172,44176,44180,44188,44189,44191,44192,44193,44200,44201,44202,44204,44207,44208,44216,44217,44219,44220,44221,44225,44228,44232,44236,44245,44247,44256,44257,44260,44263,44264,44266,44268,44271,44272,44273,44275,44277,44278,44284,44285,44288,44292,44294,52994,52995,52997,52998,52999,53001,53002,53003,53004,53005,53006,53007,53010,53012,53014,53015,53016,53017,53018,53019,53021,53022,53023,53025,53026,53027,null,null,null,null,null,null,53029,53030,53031,53032,53033,53034,53035,53038,53042,53043,53044,53045,53046,53047,53049,53050,53051,53052,53053,53054,53055,53056,53057,53058,53059,53060,null,null,null,null,null,null,53061,53062,53063,53064,53065,53066,53067,53068,53069,53070,53071,53072,53073,53074,53075,53078,53079,53081,53082,53083,53085,53086,53087,53088,53089,53090,53091,53094,53096,53098,53099,53100,44300,44301,44303,44305,44312,44316,44320,44329,44332,44333,44340,44341,44344,44348,44356,44357,44359,44361,44368,44372,44376,44385,44387,44396,44397,44400,44403,44404,44405,44406,44411,44412,44413,44415,44417,44418,44424,44425,44428,44432,44444,44445,44452,44471,44480,44481,44484,44488,44496,44497,44499,44508,44512,44516,44536,44537,44540,44543,44544,44545,44552,44553,44555,44557,44564,44592,44593,44596,44599,44600,44602,44608,44609,44611,44613,44614,44618,44620,44621,44622,44624,44628,44630,44636,44637,44639,44640,44641,44645,44648,44649,44652,44656,44664,53101,53102,53103,53106,53107,53109,53110,53111,53113,53114,53115,53116,53117,53118,53119,53121,53122,53123,53124,53126,53127,53128,53129,53130,53131,53133,null,null,null,null,null,null,53134,53135,53136,53137,53138,53139,53140,53141,53142,53143,53144,53145,53146,53147,53148,53149,53150,53151,53152,53154,53155,53156,53157,53158,53159,53161,null,null,null,null,null,null,53162,53163,53164,53165,53166,53167,53169,53170,53171,53172,53173,53174,53175,53176,53177,53178,53179,53180,53181,53182,53183,53184,53185,53186,53187,53189,53190,53191,53192,53193,53194,53195,44665,44667,44668,44669,44676,44677,44684,44732,44733,44734,44736,44740,44748,44749,44751,44752,44753,44760,44761,44764,44776,44779,44781,44788,44792,44796,44807,44808,44813,44816,44844,44845,44848,44850,44852,44860,44861,44863,44865,44866,44867,44872,44873,44880,44892,44893,44900,44901,44921,44928,44932,44936,44944,44945,44949,44956,44984,44985,44988,44992,44999,45000,45001,45003,45005,45006,45012,45020,45032,45033,45040,45041,45044,45048,45056,45057,45060,45068,45072,45076,45084,45085,45096,45124,45125,45128,45130,45132,45134,45139,45140,45141,45143,45145,53196,53197,53198,53199,53200,53201,53202,53203,53204,53205,53206,53207,53208,53209,53210,53211,53212,53213,53214,53215,53218,53219,53221,53222,53223,53225,null,null,null,null,null,null,53226,53227,53228,53229,53230,53231,53234,53236,53238,53239,53240,53241,53242,53243,53245,53246,53247,53249,53250,53251,53253,53254,53255,53256,53257,53258,null,null,null,null,null,null,53259,53260,53261,53262,53263,53264,53266,53267,53268,53269,53270,53271,53273,53274,53275,53276,53277,53278,53279,53280,53281,53282,53283,53284,53285,53286,53287,53288,53289,53290,53291,53292,45149,45180,45181,45184,45188,45196,45197,45199,45201,45208,45209,45210,45212,45215,45216,45217,45218,45224,45225,45227,45228,45229,45230,45231,45233,45235,45236,45237,45240,45244,45252,45253,45255,45256,45257,45264,45265,45268,45272,45280,45285,45320,45321,45323,45324,45328,45330,45331,45336,45337,45339,45340,45341,45347,45348,45349,45352,45356,45364,45365,45367,45368,45369,45376,45377,45380,45384,45392,45393,45396,45397,45400,45404,45408,45432,45433,45436,45440,45442,45448,45449,45451,45453,45458,45459,45460,45464,45468,45480,45516,45520,45524,45532,45533,53294,53295,53296,53297,53298,53299,53302,53303,53305,53306,53307,53309,53310,53311,53312,53313,53314,53315,53318,53320,53322,53323,53324,53325,53326,53327,null,null,null,null,null,null,53329,53330,53331,53333,53334,53335,53337,53338,53339,53340,53341,53342,53343,53345,53346,53347,53348,53349,53350,53351,53352,53353,53354,53355,53358,53359,null,null,null,null,null,null,53361,53362,53363,53365,53366,53367,53368,53369,53370,53371,53374,53375,53376,53378,53379,53380,53381,53382,53383,53384,53385,53386,53387,53388,53389,53390,53391,53392,53393,53394,53395,53396,45535,45544,45545,45548,45552,45561,45563,45565,45572,45573,45576,45579,45580,45588,45589,45591,45593,45600,45620,45628,45656,45660,45664,45672,45673,45684,45685,45692,45700,45701,45705,45712,45713,45716,45720,45721,45722,45728,45729,45731,45733,45734,45738,45740,45744,45748,45768,45769,45772,45776,45778,45784,45785,45787,45789,45794,45796,45797,45798,45800,45803,45804,45805,45806,45807,45811,45812,45813,45815,45816,45817,45818,45819,45823,45824,45825,45828,45832,45840,45841,45843,45844,45845,45852,45908,45909,45910,45912,45915,45916,45918,45919,45924,45925,53397,53398,53399,53400,53401,53402,53403,53404,53405,53406,53407,53408,53409,53410,53411,53414,53415,53417,53418,53419,53421,53422,53423,53424,53425,53426,null,null,null,null,null,null,53427,53430,53432,53434,53435,53436,53437,53438,53439,53442,53443,53445,53446,53447,53450,53451,53452,53453,53454,53455,53458,53462,53463,53464,53465,53466,null,null,null,null,null,null,53467,53470,53471,53473,53474,53475,53477,53478,53479,53480,53481,53482,53483,53486,53490,53491,53492,53493,53494,53495,53497,53498,53499,53500,53501,53502,53503,53504,53505,53506,53507,53508,45927,45929,45931,45934,45936,45937,45940,45944,45952,45953,45955,45956,45957,45964,45968,45972,45984,45985,45992,45996,46020,46021,46024,46027,46028,46030,46032,46036,46037,46039,46041,46043,46045,46048,46052,46056,46076,46096,46104,46108,46112,46120,46121,46123,46132,46160,46161,46164,46168,46176,46177,46179,46181,46188,46208,46216,46237,46244,46248,46252,46261,46263,46265,46272,46276,46280,46288,46293,46300,46301,46304,46307,46308,46310,46316,46317,46319,46321,46328,46356,46357,46360,46363,46364,46372,46373,46375,46376,46377,46378,46384,46385,46388,46392,53509,53510,53511,53512,53513,53514,53515,53516,53518,53519,53520,53521,53522,53523,53524,53525,53526,53527,53528,53529,53530,53531,53532,53533,53534,53535,null,null,null,null,null,null,53536,53537,53538,53539,53540,53541,53542,53543,53544,53545,53546,53547,53548,53549,53550,53551,53554,53555,53557,53558,53559,53561,53563,53564,53565,53566,null,null,null,null,null,null,53567,53570,53574,53575,53576,53577,53578,53579,53582,53583,53585,53586,53587,53589,53590,53591,53592,53593,53594,53595,53598,53600,53602,53603,53604,53605,53606,53607,53609,53610,53611,53613,46400,46401,46403,46404,46405,46411,46412,46413,46416,46420,46428,46429,46431,46432,46433,46496,46497,46500,46504,46506,46507,46512,46513,46515,46516,46517,46523,46524,46525,46528,46532,46540,46541,46543,46544,46545,46552,46572,46608,46609,46612,46616,46629,46636,46644,46664,46692,46696,46748,46749,46752,46756,46763,46764,46769,46804,46832,46836,46840,46848,46849,46853,46888,46889,46892,46895,46896,46904,46905,46907,46916,46920,46924,46932,46933,46944,46948,46952,46960,46961,46963,46965,46972,46973,46976,46980,46988,46989,46991,46992,46993,46994,46998,46999,53614,53615,53616,53617,53618,53619,53620,53621,53622,53623,53624,53625,53626,53627,53629,53630,53631,53632,53633,53634,53635,53637,53638,53639,53641,53642,null,null,null,null,null,null,53643,53644,53645,53646,53647,53648,53649,53650,53651,53652,53653,53654,53655,53656,53657,53658,53659,53660,53661,53662,53663,53666,53667,53669,53670,53671,null,null,null,null,null,null,53673,53674,53675,53676,53677,53678,53679,53682,53684,53686,53687,53688,53689,53691,53693,53694,53695,53697,53698,53699,53700,53701,53702,53703,53704,53705,53706,53707,53708,53709,53710,53711,47000,47001,47004,47008,47016,47017,47019,47020,47021,47028,47029,47032,47047,47049,47084,47085,47088,47092,47100,47101,47103,47104,47105,47111,47112,47113,47116,47120,47128,47129,47131,47133,47140,47141,47144,47148,47156,47157,47159,47160,47161,47168,47172,47185,47187,47196,47197,47200,47204,47212,47213,47215,47217,47224,47228,47245,47272,47280,47284,47288,47296,47297,47299,47301,47308,47312,47316,47325,47327,47329,47336,47337,47340,47344,47352,47353,47355,47357,47364,47384,47392,47420,47421,47424,47428,47436,47439,47441,47448,47449,47452,47456,47464,47465,53712,53713,53714,53715,53716,53717,53718,53719,53721,53722,53723,53724,53725,53726,53727,53728,53729,53730,53731,53732,53733,53734,53735,53736,53737,53738,null,null,null,null,null,null,53739,53740,53741,53742,53743,53744,53745,53746,53747,53749,53750,53751,53753,53754,53755,53756,53757,53758,53759,53760,53761,53762,53763,53764,53765,53766,null,null,null,null,null,null,53768,53770,53771,53772,53773,53774,53775,53777,53778,53779,53780,53781,53782,53783,53784,53785,53786,53787,53788,53789,53790,53791,53792,53793,53794,53795,53796,53797,53798,53799,53800,53801,47467,47469,47476,47477,47480,47484,47492,47493,47495,47497,47498,47501,47502,47532,47533,47536,47540,47548,47549,47551,47553,47560,47561,47564,47566,47567,47568,47569,47570,47576,47577,47579,47581,47582,47585,47587,47588,47589,47592,47596,47604,47605,47607,47608,47609,47610,47616,47617,47624,47637,47672,47673,47676,47680,47682,47688,47689,47691,47693,47694,47699,47700,47701,47704,47708,47716,47717,47719,47720,47721,47728,47729,47732,47736,47747,47748,47749,47751,47756,47784,47785,47787,47788,47792,47794,47800,47801,47803,47805,47812,47816,47832,47833,47868,53802,53803,53806,53807,53809,53810,53811,53813,53814,53815,53816,53817,53818,53819,53822,53824,53826,53827,53828,53829,53830,53831,53833,53834,53835,53836,null,null,null,null,null,null,53837,53838,53839,53840,53841,53842,53843,53844,53845,53846,53847,53848,53849,53850,53851,53853,53854,53855,53856,53857,53858,53859,53861,53862,53863,53864,null,null,null,null,null,null,53865,53866,53867,53868,53869,53870,53871,53872,53873,53874,53875,53876,53877,53878,53879,53880,53881,53882,53883,53884,53885,53886,53887,53890,53891,53893,53894,53895,53897,53898,53899,53900,47872,47876,47885,47887,47889,47896,47900,47904,47913,47915,47924,47925,47926,47928,47931,47932,47933,47934,47940,47941,47943,47945,47949,47951,47952,47956,47960,47969,47971,47980,48008,48012,48016,48036,48040,48044,48052,48055,48064,48068,48072,48080,48083,48120,48121,48124,48127,48128,48130,48136,48137,48139,48140,48141,48143,48145,48148,48149,48150,48151,48152,48155,48156,48157,48158,48159,48164,48165,48167,48169,48173,48176,48177,48180,48184,48192,48193,48195,48196,48197,48201,48204,48205,48208,48221,48260,48261,48264,48267,48268,48270,48276,48277,48279,53901,53902,53903,53906,53907,53908,53910,53911,53912,53913,53914,53915,53917,53918,53919,53921,53922,53923,53925,53926,53927,53928,53929,53930,53931,53933,null,null,null,null,null,null,53934,53935,53936,53938,53939,53940,53941,53942,53943,53946,53947,53949,53950,53953,53955,53956,53957,53958,53959,53962,53964,53965,53966,53967,53968,53969,null,null,null,null,null,null,53970,53971,53973,53974,53975,53977,53978,53979,53981,53982,53983,53984,53985,53986,53987,53990,53991,53992,53993,53994,53995,53996,53997,53998,53999,54002,54003,54005,54006,54007,54009,54010,48281,48282,48288,48289,48292,48295,48296,48304,48305,48307,48308,48309,48316,48317,48320,48324,48333,48335,48336,48337,48341,48344,48348,48372,48373,48374,48376,48380,48388,48389,48391,48393,48400,48404,48420,48428,48448,48456,48457,48460,48464,48472,48473,48484,48488,48512,48513,48516,48519,48520,48521,48522,48528,48529,48531,48533,48537,48538,48540,48548,48560,48568,48596,48597,48600,48604,48617,48624,48628,48632,48640,48643,48645,48652,48653,48656,48660,48668,48669,48671,48708,48709,48712,48716,48718,48724,48725,48727,48729,48730,48731,48736,48737,48740,54011,54012,54013,54014,54015,54018,54020,54022,54023,54024,54025,54026,54027,54031,54033,54034,54035,54037,54039,54040,54041,54042,54043,54046,54050,54051,null,null,null,null,null,null,54052,54054,54055,54058,54059,54061,54062,54063,54065,54066,54067,54068,54069,54070,54071,54074,54078,54079,54080,54081,54082,54083,54086,54087,54088,54089,null,null,null,null,null,null,54090,54091,54092,54093,54094,54095,54096,54097,54098,54099,54100,54101,54102,54103,54104,54105,54106,54107,54108,54109,54110,54111,54112,54113,54114,54115,54116,54117,54118,54119,54120,54121,48744,48746,48752,48753,48755,48756,48757,48763,48764,48765,48768,48772,48780,48781,48783,48784,48785,48792,48793,48808,48848,48849,48852,48855,48856,48864,48867,48868,48869,48876,48897,48904,48905,48920,48921,48923,48924,48925,48960,48961,48964,48968,48976,48977,48981,49044,49072,49093,49100,49101,49104,49108,49116,49119,49121,49212,49233,49240,49244,49248,49256,49257,49296,49297,49300,49304,49312,49313,49315,49317,49324,49325,49327,49328,49331,49332,49333,49334,49340,49341,49343,49344,49345,49349,49352,49353,49356,49360,49368,49369,49371,49372,49373,49380,54122,54123,54124,54125,54126,54127,54128,54129,54130,54131,54132,54133,54134,54135,54136,54137,54138,54139,54142,54143,54145,54146,54147,54149,54150,54151,null,null,null,null,null,null,54152,54153,54154,54155,54158,54162,54163,54164,54165,54166,54167,54170,54171,54173,54174,54175,54177,54178,54179,54180,54181,54182,54183,54186,54188,54190,null,null,null,null,null,null,54191,54192,54193,54194,54195,54197,54198,54199,54201,54202,54203,54205,54206,54207,54208,54209,54210,54211,54214,54215,54218,54219,54220,54221,54222,54223,54225,54226,54227,54228,54229,54230,49381,49384,49388,49396,49397,49399,49401,49408,49412,49416,49424,49429,49436,49437,49438,49439,49440,49443,49444,49446,49447,49452,49453,49455,49456,49457,49462,49464,49465,49468,49472,49480,49481,49483,49484,49485,49492,49493,49496,49500,49508,49509,49511,49512,49513,49520,49524,49528,49541,49548,49549,49550,49552,49556,49558,49564,49565,49567,49569,49573,49576,49577,49580,49584,49597,49604,49608,49612,49620,49623,49624,49632,49636,49640,49648,49649,49651,49660,49661,49664,49668,49676,49677,49679,49681,49688,49689,49692,49695,49696,49704,49705,49707,49709,54231,54233,54234,54235,54236,54237,54238,54239,54240,54242,54244,54245,54246,54247,54248,54249,54250,54251,54254,54255,54257,54258,54259,54261,54262,54263,null,null,null,null,null,null,54264,54265,54266,54267,54270,54272,54274,54275,54276,54277,54278,54279,54281,54282,54283,54284,54285,54286,54287,54288,54289,54290,54291,54292,54293,54294,null,null,null,null,null,null,54295,54296,54297,54298,54299,54300,54302,54303,54304,54305,54306,54307,54308,54309,54310,54311,54312,54313,54314,54315,54316,54317,54318,54319,54320,54321,54322,54323,54324,54325,54326,54327,49711,49713,49714,49716,49736,49744,49745,49748,49752,49760,49765,49772,49773,49776,49780,49788,49789,49791,49793,49800,49801,49808,49816,49819,49821,49828,49829,49832,49836,49837,49844,49845,49847,49849,49884,49885,49888,49891,49892,49899,49900,49901,49903,49905,49910,49912,49913,49915,49916,49920,49928,49929,49932,49933,49939,49940,49941,49944,49948,49956,49957,49960,49961,49989,50024,50025,50028,50032,50034,50040,50041,50044,50045,50052,50056,50060,50112,50136,50137,50140,50143,50144,50146,50152,50153,50157,50164,50165,50168,50184,50192,50212,50220,50224,54328,54329,54330,54331,54332,54333,54334,54335,54337,54338,54339,54341,54342,54343,54344,54345,54346,54347,54348,54349,54350,54351,54352,54353,54354,54355,null,null,null,null,null,null,54356,54357,54358,54359,54360,54361,54362,54363,54365,54366,54367,54369,54370,54371,54373,54374,54375,54376,54377,54378,54379,54380,54382,54384,54385,54386,null,null,null,null,null,null,54387,54388,54389,54390,54391,54394,54395,54397,54398,54401,54403,54404,54405,54406,54407,54410,54412,54414,54415,54416,54417,54418,54419,54421,54422,54423,54424,54425,54426,54427,54428,54429,50228,50236,50237,50248,50276,50277,50280,50284,50292,50293,50297,50304,50324,50332,50360,50364,50409,50416,50417,50420,50424,50426,50431,50432,50433,50444,50448,50452,50460,50472,50473,50476,50480,50488,50489,50491,50493,50500,50501,50504,50505,50506,50508,50509,50510,50515,50516,50517,50519,50520,50521,50525,50526,50528,50529,50532,50536,50544,50545,50547,50548,50549,50556,50557,50560,50564,50567,50572,50573,50575,50577,50581,50583,50584,50588,50592,50601,50612,50613,50616,50617,50619,50620,50621,50622,50628,50629,50630,50631,50632,50633,50634,50636,50638,54430,54431,54432,54433,54434,54435,54436,54437,54438,54439,54440,54442,54443,54444,54445,54446,54447,54448,54449,54450,54451,54452,54453,54454,54455,54456,null,null,null,null,null,null,54457,54458,54459,54460,54461,54462,54463,54464,54465,54466,54467,54468,54469,54470,54471,54472,54473,54474,54475,54477,54478,54479,54481,54482,54483,54485,null,null,null,null,null,null,54486,54487,54488,54489,54490,54491,54493,54494,54496,54497,54498,54499,54500,54501,54502,54503,54505,54506,54507,54509,54510,54511,54513,54514,54515,54516,54517,54518,54519,54521,54522,54524,50640,50641,50644,50648,50656,50657,50659,50661,50668,50669,50670,50672,50676,50678,50679,50684,50685,50686,50687,50688,50689,50693,50694,50695,50696,50700,50704,50712,50713,50715,50716,50724,50725,50728,50732,50733,50734,50736,50739,50740,50741,50743,50745,50747,50752,50753,50756,50760,50768,50769,50771,50772,50773,50780,50781,50784,50796,50799,50801,50808,50809,50812,50816,50824,50825,50827,50829,50836,50837,50840,50844,50852,50853,50855,50857,50864,50865,50868,50872,50873,50874,50880,50881,50883,50885,50892,50893,50896,50900,50908,50909,50912,50913,50920,54526,54527,54528,54529,54530,54531,54533,54534,54535,54537,54538,54539,54541,54542,54543,54544,54545,54546,54547,54550,54552,54553,54554,54555,54556,54557,null,null,null,null,null,null,54558,54559,54560,54561,54562,54563,54564,54565,54566,54567,54568,54569,54570,54571,54572,54573,54574,54575,54576,54577,54578,54579,54580,54581,54582,54583,null,null,null,null,null,null,54584,54585,54586,54587,54590,54591,54593,54594,54595,54597,54598,54599,54600,54601,54602,54603,54606,54608,54610,54611,54612,54613,54614,54615,54618,54619,54621,54622,54623,54625,54626,54627,50921,50924,50928,50936,50937,50941,50948,50949,50952,50956,50964,50965,50967,50969,50976,50977,50980,50984,50992,50993,50995,50997,50999,51004,51005,51008,51012,51018,51020,51021,51023,51025,51026,51027,51028,51029,51030,51031,51032,51036,51040,51048,51051,51060,51061,51064,51068,51069,51070,51075,51076,51077,51079,51080,51081,51082,51086,51088,51089,51092,51094,51095,51096,51098,51104,51105,51107,51108,51109,51110,51116,51117,51120,51124,51132,51133,51135,51136,51137,51144,51145,51148,51150,51152,51160,51165,51172,51176,51180,51200,51201,51204,51208,51210,54628,54630,54631,54634,54636,54638,54639,54640,54641,54642,54643,54646,54647,54649,54650,54651,54653,54654,54655,54656,54657,54658,54659,54662,54666,54667,null,null,null,null,null,null,54668,54669,54670,54671,54673,54674,54675,54676,54677,54678,54679,54680,54681,54682,54683,54684,54685,54686,54687,54688,54689,54690,54691,54692,54694,54695,null,null,null,null,null,null,54696,54697,54698,54699,54700,54701,54702,54703,54704,54705,54706,54707,54708,54709,54710,54711,54712,54713,54714,54715,54716,54717,54718,54719,54720,54721,54722,54723,54724,54725,54726,54727,51216,51217,51219,51221,51222,51228,51229,51232,51236,51244,51245,51247,51249,51256,51260,51264,51272,51273,51276,51277,51284,51312,51313,51316,51320,51322,51328,51329,51331,51333,51334,51335,51339,51340,51341,51348,51357,51359,51361,51368,51388,51389,51396,51400,51404,51412,51413,51415,51417,51424,51425,51428,51445,51452,51453,51456,51460,51461,51462,51468,51469,51471,51473,51480,51500,51508,51536,51537,51540,51544,51552,51553,51555,51564,51568,51572,51580,51592,51593,51596,51600,51608,51609,51611,51613,51648,51649,51652,51655,51656,51658,51664,51665,51667,54730,54731,54733,54734,54735,54737,54739,54740,54741,54742,54743,54746,54748,54750,54751,54752,54753,54754,54755,54758,54759,54761,54762,54763,54765,54766,null,null,null,null,null,null,54767,54768,54769,54770,54771,54774,54776,54778,54779,54780,54781,54782,54783,54786,54787,54789,54790,54791,54793,54794,54795,54796,54797,54798,54799,54802,null,null,null,null,null,null,54806,54807,54808,54809,54810,54811,54813,54814,54815,54817,54818,54819,54821,54822,54823,54824,54825,54826,54827,54828,54830,54831,54832,54833,54834,54835,54836,54837,54838,54839,54842,54843,51669,51670,51673,51674,51676,51677,51680,51682,51684,51687,51692,51693,51695,51696,51697,51704,51705,51708,51712,51720,51721,51723,51724,51725,51732,51736,51753,51788,51789,51792,51796,51804,51805,51807,51808,51809,51816,51837,51844,51864,51900,51901,51904,51908,51916,51917,51919,51921,51923,51928,51929,51936,51948,51956,51976,51984,51988,51992,52000,52001,52033,52040,52041,52044,52048,52056,52057,52061,52068,52088,52089,52124,52152,52180,52196,52199,52201,52236,52237,52240,52244,52252,52253,52257,52258,52263,52264,52265,52268,52270,52272,52280,52281,52283,54845,54846,54847,54849,54850,54851,54852,54854,54855,54858,54860,54862,54863,54864,54866,54867,54870,54871,54873,54874,54875,54877,54878,54879,54880,54881,null,null,null,null,null,null,54882,54883,54884,54885,54886,54888,54890,54891,54892,54893,54894,54895,54898,54899,54901,54902,54903,54904,54905,54906,54907,54908,54909,54910,54911,54912,null,null,null,null,null,null,54913,54914,54916,54918,54919,54920,54921,54922,54923,54926,54927,54929,54930,54931,54933,54934,54935,54936,54937,54938,54939,54940,54942,54944,54946,54947,54948,54949,54950,54951,54953,54954,52284,52285,52286,52292,52293,52296,52300,52308,52309,52311,52312,52313,52320,52324,52326,52328,52336,52341,52376,52377,52380,52384,52392,52393,52395,52396,52397,52404,52405,52408,52412,52420,52421,52423,52425,52432,52436,52452,52460,52464,52481,52488,52489,52492,52496,52504,52505,52507,52509,52516,52520,52524,52537,52572,52576,52580,52588,52589,52591,52593,52600,52616,52628,52629,52632,52636,52644,52645,52647,52649,52656,52676,52684,52688,52712,52716,52720,52728,52729,52731,52733,52740,52744,52748,52756,52761,52768,52769,52772,52776,52784,52785,52787,52789,54955,54957,54958,54959,54961,54962,54963,54964,54965,54966,54967,54968,54970,54972,54973,54974,54975,54976,54977,54978,54979,54982,54983,54985,54986,54987,null,null,null,null,null,null,54989,54990,54991,54992,54994,54995,54997,54998,55000,55002,55003,55004,55005,55006,55007,55009,55010,55011,55013,55014,55015,55017,55018,55019,55020,55021,null,null,null,null,null,null,55022,55023,55025,55026,55027,55028,55030,55031,55032,55033,55034,55035,55038,55039,55041,55042,55043,55045,55046,55047,55048,55049,55050,55051,55052,55053,55054,55055,55056,55058,55059,55060,52824,52825,52828,52831,52832,52833,52840,52841,52843,52845,52852,52853,52856,52860,52868,52869,52871,52873,52880,52881,52884,52888,52896,52897,52899,52900,52901,52908,52909,52929,52964,52965,52968,52971,52972,52980,52981,52983,52984,52985,52992,52993,52996,53000,53008,53009,53011,53013,53020,53024,53028,53036,53037,53039,53040,53041,53048,53076,53077,53080,53084,53092,53093,53095,53097,53104,53105,53108,53112,53120,53125,53132,53153,53160,53168,53188,53216,53217,53220,53224,53232,53233,53235,53237,53244,53248,53252,53265,53272,53293,53300,53301,53304,53308,55061,55062,55063,55066,55067,55069,55070,55071,55073,55074,55075,55076,55077,55078,55079,55082,55084,55086,55087,55088,55089,55090,55091,55094,55095,55097,null,null,null,null,null,null,55098,55099,55101,55102,55103,55104,55105,55106,55107,55109,55110,55112,55114,55115,55116,55117,55118,55119,55122,55123,55125,55130,55131,55132,55133,55134,null,null,null,null,null,null,55135,55138,55140,55142,55143,55144,55146,55147,55149,55150,55151,55153,55154,55155,55157,55158,55159,55160,55161,55162,55163,55166,55167,55168,55170,55171,55172,55173,55174,55175,55178,55179,53316,53317,53319,53321,53328,53332,53336,53344,53356,53357,53360,53364,53372,53373,53377,53412,53413,53416,53420,53428,53429,53431,53433,53440,53441,53444,53448,53449,53456,53457,53459,53460,53461,53468,53469,53472,53476,53484,53485,53487,53488,53489,53496,53517,53552,53553,53556,53560,53562,53568,53569,53571,53572,53573,53580,53581,53584,53588,53596,53597,53599,53601,53608,53612,53628,53636,53640,53664,53665,53668,53672,53680,53681,53683,53685,53690,53692,53696,53720,53748,53752,53767,53769,53776,53804,53805,53808,53812,53820,53821,53823,53825,53832,53852,55181,55182,55183,55185,55186,55187,55188,55189,55190,55191,55194,55196,55198,55199,55200,55201,55202,55203,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,53860,53888,53889,53892,53896,53904,53905,53909,53916,53920,53924,53932,53937,53944,53945,53948,53951,53952,53954,53960,53961,53963,53972,53976,53980,53988,53989,54000,54001,54004,54008,54016,54017,54019,54021,54028,54029,54030,54032,54036,54038,54044,54045,54047,54048,54049,54053,54056,54057,54060,54064,54072,54073,54075,54076,54077,54084,54085,54140,54141,54144,54148,54156,54157,54159,54160,54161,54168,54169,54172,54176,54184,54185,54187,54189,54196,54200,54204,54212,54213,54216,54217,54224,54232,54241,54243,54252,54253,54256,54260,54268,54269,54271,54273,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,54280,54301,54336,54340,54364,54368,54372,54381,54383,54392,54393,54396,54399,54400,54402,54408,54409,54411,54413,54420,54441,54476,54480,54484,54492,54495,54504,54508,54512,54520,54523,54525,54532,54536,54540,54548,54549,54551,54588,54589,54592,54596,54604,54605,54607,54609,54616,54617,54620,54624,54629,54632,54633,54635,54637,54644,54645,54648,54652,54660,54661,54663,54664,54665,54672,54693,54728,54729,54732,54736,54738,54744,54745,54747,54749,54756,54757,54760,54764,54772,54773,54775,54777,54784,54785,54788,54792,54800,54801,54803,54804,54805,54812,54816,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,54820,54829,54840,54841,54844,54848,54853,54856,54857,54859,54861,54865,54868,54869,54872,54876,54887,54889,54896,54897,54900,54915,54917,54924,54925,54928,54932,54941,54943,54945,54952,54956,54960,54969,54971,54980,54981,54984,54988,54993,54996,54999,55001,55008,55012,55016,55024,55029,55036,55037,55040,55044,55057,55064,55065,55068,55072,55080,55081,55083,55085,55092,55093,55096,55100,55108,55111,55113,55120,55121,55124,55126,55127,55128,55129,55136,55137,55139,55141,55145,55148,55152,55156,55164,55165,55169,55176,55177,55180,55184,55192,55193,55195,55197,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20285,20339,20551,20729,21152,21487,21621,21733,22025,23233,23478,26247,26550,26551,26607,27468,29634,30146,31292,33499,33540,34903,34952,35382,36040,36303,36603,36838,39381,21051,21364,21508,24682,24932,27580,29647,33050,35258,35282,38307,20355,21002,22718,22904,23014,24178,24185,25031,25536,26438,26604,26751,28567,30286,30475,30965,31240,31487,31777,32925,33390,33393,35563,38291,20075,21917,26359,28212,30883,31469,33883,35088,34638,38824,21208,22350,22570,23884,24863,25022,25121,25954,26577,27204,28187,29976,30131,30435,30640,32058,37039,37969,37970,40853,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21283,23724,30002,32987,37440,38296,21083,22536,23004,23713,23831,24247,24378,24394,24951,27743,30074,30086,31968,32115,32177,32652,33108,33313,34193,35137,35611,37628,38477,40007,20171,20215,20491,20977,22607,24887,24894,24936,25913,27114,28433,30117,30342,30422,31623,33445,33995,63744,37799,38283,21888,23458,22353,63745,31923,32697,37301,20520,21435,23621,24040,25298,25454,25818,25831,28192,28844,31067,36317,36382,63746,36989,37445,37624,20094,20214,20581,24062,24314,24838,26967,33137,34388,36423,37749,39467,20062,20625,26480,26688,20745,21133,21138,27298,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,30652,37392,40660,21163,24623,36850,20552,25001,25581,25802,26684,27268,28608,33160,35233,38548,22533,29309,29356,29956,32121,32365,32937,35211,35700,36963,40273,25225,27770,28500,32080,32570,35363,20860,24906,31645,35609,37463,37772,20140,20435,20510,20670,20742,21185,21197,21375,22384,22659,24218,24465,24950,25004,25806,25964,26223,26299,26356,26775,28039,28805,28913,29855,29861,29898,30169,30828,30956,31455,31478,32069,32147,32789,32831,33051,33686,35686,36629,36885,37857,38915,38968,39514,39912,20418,21843,22586,22865,23395,23622,24760,25106,26690,26800,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26856,28330,30028,30328,30926,31293,31995,32363,32380,35336,35489,35903,38542,40388,21476,21481,21578,21617,22266,22993,23396,23611,24235,25335,25911,25925,25970,26272,26543,27073,27837,30204,30352,30590,31295,32660,32771,32929,33167,33510,33533,33776,34241,34865,34996,35493,63747,36764,37678,38599,39015,39640,40723,21741,26011,26354,26767,31296,35895,40288,22256,22372,23825,26118,26801,26829,28414,29736,34974,39908,27752,63748,39592,20379,20844,20849,21151,23380,24037,24656,24685,25329,25511,25915,29657,31354,34467,36002,38799,20018,23521,25096,26524,29916,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31185,33747,35463,35506,36328,36942,37707,38982,24275,27112,34303,37101,63749,20896,23448,23532,24931,26874,27454,28748,29743,29912,31649,32592,33733,35264,36011,38364,39208,21038,24669,25324,36866,20362,20809,21281,22745,24291,26336,27960,28826,29378,29654,31568,33009,37979,21350,25499,32619,20054,20608,22602,22750,24618,24871,25296,27088,39745,23439,32024,32945,36703,20132,20689,21676,21932,23308,23968,24039,25898,25934,26657,27211,29409,30350,30703,32094,32761,33184,34126,34527,36611,36686,37066,39171,39509,39851,19992,20037,20061,20167,20465,20855,21246,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21312,21475,21477,21646,22036,22389,22434,23495,23943,24272,25084,25304,25937,26552,26601,27083,27472,27590,27628,27714,28317,28792,29399,29590,29699,30655,30697,31350,32127,32777,33276,33285,33290,33503,34914,35635,36092,36544,36881,37041,37476,37558,39378,39493,40169,40407,40860,22283,23616,33738,38816,38827,40628,21531,31384,32676,35033,36557,37089,22528,23624,25496,31391,23470,24339,31353,31406,33422,36524,20518,21048,21240,21367,22280,25331,25458,27402,28099,30519,21413,29527,34152,36470,38357,26426,27331,28528,35437,36556,39243,63750,26231,27512,36020,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,39740,63751,21483,22317,22862,25542,27131,29674,30789,31418,31429,31998,33909,35215,36211,36917,38312,21243,22343,30023,31584,33740,37406,63752,27224,20811,21067,21127,25119,26840,26997,38553,20677,21156,21220,25027,26020,26681,27135,29822,31563,33465,33771,35250,35641,36817,39241,63753,20170,22935,25810,26129,27278,29748,31105,31165,33449,34942,34943,35167,63754,37670,20235,21450,24613,25201,27762,32026,32102,20120,20834,30684,32943,20225,20238,20854,20864,21980,22120,22331,22522,22524,22804,22855,22931,23492,23696,23822,24049,24190,24524,25216,26071,26083,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26398,26399,26462,26827,26820,27231,27450,27683,27773,27778,28103,29592,29734,29738,29826,29859,30072,30079,30849,30959,31041,31047,31048,31098,31637,32000,32186,32648,32774,32813,32908,35352,35663,35912,36215,37665,37668,39138,39249,39438,39439,39525,40594,32202,20342,21513,25326,26708,37329,21931,20794,63755,63756,23068,25062,63757,25295,25343,63758,63759,63760,63761,63762,63763,37027,63764,63765,63766,63767,63768,35582,63769,63770,63771,63772,26262,63773,29014,63774,63775,38627,63776,25423,25466,21335,63777,26511,26976,28275,63778,30007,63779,63780,63781,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32013,63782,63783,34930,22218,23064,63784,63785,63786,63787,63788,20035,63789,20839,22856,26608,32784,63790,22899,24180,25754,31178,24565,24684,25288,25467,23527,23511,21162,63791,22900,24361,24594,63792,63793,63794,29785,63795,63796,63797,63798,63799,63800,39377,63801,63802,63803,63804,63805,63806,63807,63808,63809,63810,63811,28611,63812,63813,33215,36786,24817,63814,63815,33126,63816,63817,23615,63818,63819,63820,63821,63822,63823,63824,63825,23273,35365,26491,32016,63826,63827,63828,63829,63830,63831,33021,63832,63833,23612,27877,21311,28346,22810,33590,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20025,20150,20294,21934,22296,22727,24406,26039,26086,27264,27573,28237,30701,31471,31774,32222,34507,34962,37170,37723,25787,28606,29562,30136,36948,21846,22349,25018,25812,26311,28129,28251,28525,28601,30192,32835,33213,34113,35203,35527,35674,37663,27795,30035,31572,36367,36957,21776,22530,22616,24162,25095,25758,26848,30070,31958,34739,40680,20195,22408,22382,22823,23565,23729,24118,24453,25140,25825,29619,33274,34955,36024,38538,40667,23429,24503,24755,20498,20992,21040,22294,22581,22615,23566,23648,23798,23947,24230,24466,24764,25361,25481,25623,26691,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26873,27330,28120,28193,28372,28644,29182,30428,30585,31153,31291,33796,35241,36077,36339,36424,36867,36884,36947,37117,37709,38518,38876,27602,28678,29272,29346,29544,30563,31167,31716,32411,35712,22697,24775,25958,26109,26302,27788,28958,29129,35930,38931,20077,31361,20189,20908,20941,21205,21516,24999,26481,26704,26847,27934,28540,30140,30643,31461,33012,33891,37509,20828,26007,26460,26515,30168,31431,33651,63834,35910,36887,38957,23663,33216,33434,36929,36975,37389,24471,23965,27225,29128,30331,31561,34276,35588,37159,39472,21895,25078,63835,30313,32645,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,34367,34746,35064,37007,63836,27931,28889,29662,32097,33853,63837,37226,39409,63838,20098,21365,27396,27410,28734,29211,34349,40478,21068,36771,23888,25829,25900,27414,28651,31811,32412,34253,35172,35261,25289,33240,34847,24266,26391,28010,29436,29701,29807,34690,37086,20358,23821,24480,33802,20919,25504,30053,20142,20486,20841,20937,26753,27153,31918,31921,31975,33391,35538,36635,37327,20406,20791,21237,21570,24300,24942,25150,26053,27354,28670,31018,34268,34851,38317,39522,39530,40599,40654,21147,26310,27511,28701,31019,36706,38722,24976,25088,25891,28451,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,29001,29833,32244,32879,34030,36646,36899,37706,20925,21015,21155,27916,28872,35010,24265,25986,27566,28610,31806,29557,20196,20278,22265,63839,23738,23994,24604,29618,31533,32666,32718,32838,36894,37428,38646,38728,38936,40801,20363,28583,31150,37300,38583,21214,63840,25736,25796,27347,28510,28696,29200,30439,32769,34310,34396,36335,36613,38706,39791,40442,40565,30860,31103,32160,33737,37636,40575,40595,35542,22751,24324,26407,28711,29903,31840,32894,20769,28712,29282,30922,36034,36058,36084,38647,20102,20698,23534,24278,26009,29134,30274,30637,32842,34044,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36988,39719,40845,22744,23105,23650,27155,28122,28431,30267,32047,32311,34078,35128,37860,38475,21129,26066,26611,27060,27969,28316,28687,29705,29792,30041,30244,30827,35628,39006,20845,25134,38520,20374,20523,23833,28138,32184,36650,24459,24900,26647,63841,38534,21202,32907,20956,20940,26974,31260,32190,33777,38517,20442,21033,21400,21519,21774,23653,24743,26446,26792,28012,29313,29432,29702,29827,63842,30178,31852,32633,32696,33673,35023,35041,37324,37328,38626,39881,21533,28542,29136,29848,34298,36522,38563,40023,40607,26519,28107,29747,33256,38678,30764,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31435,31520,31890,25705,29802,30194,30908,30952,39340,39764,40635,23518,24149,28448,33180,33707,37000,19975,21325,23081,24018,24398,24930,25405,26217,26364,28415,28459,28771,30622,33836,34067,34875,36627,39237,39995,21788,25273,26411,27819,33545,35178,38778,20129,22916,24536,24537,26395,32178,32596,33426,33579,33725,36638,37017,22475,22969,23186,23504,26151,26522,26757,27599,29028,32629,36023,36067,36993,39749,33032,35978,38476,39488,40613,23391,27667,29467,30450,30431,33804,20906,35219,20813,20885,21193,26825,27796,30468,30496,32191,32236,38754,40629,28357,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,34065,20901,21517,21629,26126,26269,26919,28319,30399,30609,33559,33986,34719,37225,37528,40180,34946,20398,20882,21215,22982,24125,24917,25720,25721,26286,26576,27169,27597,27611,29279,29281,29761,30520,30683,32791,33468,33541,35584,35624,35980,26408,27792,29287,30446,30566,31302,40361,27519,27794,22818,26406,33945,21359,22675,22937,24287,25551,26164,26483,28218,29483,31447,33495,37672,21209,24043,25006,25035,25098,25287,25771,26080,26969,27494,27595,28961,29687,30045,32326,33310,33538,34154,35491,36031,38695,40289,22696,40664,20497,21006,21563,21839,25991,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,27766,32010,32011,32862,34442,38272,38639,21247,27797,29289,21619,23194,23614,23883,24396,24494,26410,26806,26979,28220,28228,30473,31859,32654,34183,35598,36855,38753,40692,23735,24758,24845,25003,25935,26107,26108,27665,27887,29599,29641,32225,38292,23494,34588,35600,21085,21338,25293,25615,25778,26420,27192,27850,29632,29854,31636,31893,32283,33162,33334,34180,36843,38649,39361,20276,21322,21453,21467,25292,25644,25856,26001,27075,27886,28504,29677,30036,30242,30436,30460,30928,30971,31020,32070,33324,34784,36820,38930,39151,21187,25300,25765,28196,28497,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,30332,36299,37297,37474,39662,39747,20515,20621,22346,22952,23592,24135,24439,25151,25918,26041,26049,26121,26507,27036,28354,30917,32033,32938,33152,33323,33459,33953,34444,35370,35607,37030,38450,40848,20493,20467,63843,22521,24472,25308,25490,26479,28227,28953,30403,32972,32986,35060,35061,35097,36064,36649,37197,38506,20271,20336,24091,26575,26658,30333,30334,39748,24161,27146,29033,29140,30058,63844,32321,34115,34281,39132,20240,31567,32624,38309,20961,24070,26805,27710,27726,27867,29359,31684,33539,27861,29754,20731,21128,22721,25816,27287,29863,30294,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,30887,34327,38370,38713,63845,21342,24321,35722,36776,36783,37002,21029,30629,40009,40712,19993,20482,20853,23643,24183,26142,26170,26564,26821,28851,29953,30149,31177,31453,36647,39200,39432,20445,22561,22577,23542,26222,27493,27921,28282,28541,29668,29995,33769,35036,35091,35676,36628,20239,20693,21264,21340,23443,24489,26381,31119,33145,33583,34068,35079,35206,36665,36667,39333,39954,26412,20086,20472,22857,23553,23791,23792,25447,26834,28925,29090,29739,32299,34028,34562,36898,37586,40179,19981,20184,20463,20613,21078,21103,21542,21648,22496,22827,23142,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,23386,23413,23500,24220,63846,25206,25975,26023,28014,28325,29238,31526,31807,32566,33104,33105,33178,33344,33433,33705,35331,36000,36070,36091,36212,36282,37096,37340,38428,38468,39385,40167,21271,20998,21545,22132,22707,22868,22894,24575,24996,25198,26128,27774,28954,30406,31881,31966,32027,33452,36033,38640,63847,20315,24343,24447,25282,23849,26379,26842,30844,32323,40300,19989,20633,21269,21290,21329,22915,23138,24199,24754,24970,25161,25209,26000,26503,27047,27604,27606,27607,27608,27832,63848,29749,30202,30738,30865,31189,31192,31875,32203,32737,32933,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,33086,33218,33778,34586,35048,35513,35692,36027,37145,38750,39131,40763,22188,23338,24428,25996,27315,27567,27996,28657,28693,29277,29613,36007,36051,38971,24977,27703,32856,39425,20045,20107,20123,20181,20282,20284,20351,20447,20735,21490,21496,21766,21987,22235,22763,22882,23057,23531,23546,23556,24051,24107,24473,24605,25448,26012,26031,26614,26619,26797,27515,27801,27863,28195,28681,29509,30722,31038,31040,31072,31169,31721,32023,32114,32902,33293,33678,34001,34503,35039,35408,35422,35613,36060,36198,36781,37034,39164,39391,40605,21066,63849,26388,63850,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20632,21034,23665,25955,27733,29642,29987,30109,31639,33948,37240,38704,20087,25746,27578,29022,34217,19977,63851,26441,26862,28183,33439,34072,34923,25591,28545,37394,39087,19978,20663,20687,20767,21830,21930,22039,23360,23577,23776,24120,24202,24224,24258,24819,26705,27233,28248,29245,29248,29376,30456,31077,31665,32724,35059,35316,35443,35937,36062,38684,22622,29885,36093,21959,63852,31329,32034,33394,29298,29983,29989,63853,31513,22661,22779,23996,24207,24246,24464,24661,25234,25471,25933,26257,26329,26360,26646,26866,29312,29790,31598,32110,32214,32626,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32997,33298,34223,35199,35475,36893,37604,40653,40736,22805,22893,24109,24796,26132,26227,26512,27728,28101,28511,30707,30889,33990,37323,37675,20185,20682,20808,21892,23307,23459,25159,25982,26059,28210,29053,29697,29764,29831,29887,30316,31146,32218,32341,32680,33146,33203,33337,34330,34796,35445,36323,36984,37521,37925,39245,39854,21352,23633,26964,27844,27945,28203,33292,34203,35131,35373,35498,38634,40807,21089,26297,27570,32406,34814,36109,38275,38493,25885,28041,29166,63854,22478,22995,23468,24615,24826,25104,26143,26207,29481,29689,30427,30465,31596,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32854,32882,33125,35488,37266,19990,21218,27506,27927,31237,31545,32048,63855,36016,21484,22063,22609,23477,23567,23569,24034,25152,25475,25620,26157,26803,27836,28040,28335,28703,28836,29138,29990,30095,30094,30233,31505,31712,31787,32032,32057,34092,34157,34311,35380,36877,36961,37045,37559,38902,39479,20439,23660,26463,28049,31903,32396,35606,36118,36895,23403,24061,25613,33984,36956,39137,29575,23435,24730,26494,28126,35359,35494,36865,38924,21047,63856,28753,30862,37782,34928,37335,20462,21463,22013,22234,22402,22781,23234,23432,23723,23744,24101,24833,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,25101,25163,25480,25628,25910,25976,27193,27530,27700,27929,28465,29159,29417,29560,29703,29874,30246,30561,31168,31319,31466,31929,32143,32172,32353,32670,33065,33585,33936,34010,34282,34966,35504,35728,36664,36930,36995,37228,37526,37561,38539,38567,38568,38614,38656,38920,39318,39635,39706,21460,22654,22809,23408,23487,28113,28506,29087,29729,29881,32901,33789,24033,24455,24490,24642,26092,26642,26991,27219,27529,27957,28147,29667,30462,30636,31565,32020,33059,33308,33600,34036,34147,35426,35524,37255,37662,38918,39348,25100,34899,36848,37477,23815,23847,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,23913,29791,33181,34664,28629,25342,32722,35126,35186,19998,20056,20711,21213,21319,25215,26119,32361,34821,38494,20365,21273,22070,22987,23204,23608,23630,23629,24066,24337,24643,26045,26159,26178,26558,26612,29468,30690,31034,32709,33940,33997,35222,35430,35433,35553,35925,35962,22516,23508,24335,24687,25325,26893,27542,28252,29060,31698,34645,35672,36606,39135,39166,20280,20353,20449,21627,23072,23480,24892,26032,26216,29180,30003,31070,32051,33102,33251,33688,34218,34254,34563,35338,36523,36763,63857,36805,22833,23460,23526,24713,23529,23563,24515,27777,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63858,28145,28683,29978,33455,35574,20160,21313,63859,38617,27663,20126,20420,20818,21854,23077,23784,25105,29273,33469,33706,34558,34905,35357,38463,38597,39187,40201,40285,22538,23731,23997,24132,24801,24853,25569,27138,28197,37122,37716,38990,39952,40823,23433,23736,25353,26191,26696,30524,38593,38797,38996,39839,26017,35585,36555,38332,21813,23721,24022,24245,26263,30284,33780,38343,22739,25276,29390,40232,20208,22830,24591,26171,27523,31207,40230,21395,21696,22467,23830,24859,26326,28079,30861,33406,38552,38724,21380,25212,25494,28082,32266,33099,38989,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,27387,32588,40367,40474,20063,20539,20918,22812,24825,25590,26928,29242,32822,63860,37326,24369,63861,63862,32004,33509,33903,33979,34277,36493,63863,20335,63864,63865,22756,23363,24665,25562,25880,25965,26264,63866,26954,27171,27915,28673,29036,30162,30221,31155,31344,63867,32650,63868,35140,63869,35731,37312,38525,63870,39178,22276,24481,26044,28417,30208,31142,35486,39341,39770,40812,20740,25014,25233,27277,33222,20547,22576,24422,28937,35328,35578,23420,34326,20474,20796,22196,22852,25513,28153,23978,26989,20870,20104,20313,63871,63872,63873,22914,63874,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63875,27487,27741,63876,29877,30998,63877,33287,33349,33593,36671,36701,63878,39192,63879,63880,63881,20134,63882,22495,24441,26131,63883,63884,30123,32377,35695,63885,36870,39515,22181,22567,23032,23071,23476,63886,24310,63887,63888,25424,25403,63889,26941,27783,27839,28046,28051,28149,28436,63890,28895,28982,29017,63891,29123,29141,63892,30799,30831,63893,31605,32227,63894,32303,63895,34893,36575,63896,63897,63898,37467,63899,40182,63900,63901,63902,24709,28037,63903,29105,63904,63905,38321,21421,63906,63907,63908,26579,63909,28814,28976,29744,33398,33490,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63910,38331,39653,40573,26308,63911,29121,33865,63912,63913,22603,63914,63915,23992,24433,63916,26144,26254,27001,27054,27704,27891,28214,28481,28634,28699,28719,29008,29151,29552,63917,29787,63918,29908,30408,31310,32403,63919,63920,33521,35424,36814,63921,37704,63922,38681,63923,63924,20034,20522,63925,21000,21473,26355,27757,28618,29450,30591,31330,33454,34269,34306,63926,35028,35427,35709,35947,63927,37555,63928,38675,38928,20116,20237,20425,20658,21320,21566,21555,21978,22626,22714,22887,23067,23524,24735,63929,25034,25942,26111,26212,26791,27738,28595,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,28879,29100,29522,31613,34568,35492,39986,40711,23627,27779,29508,29577,37434,28331,29797,30239,31337,32277,34314,20800,22725,25793,29934,29973,30320,32705,37013,38605,39252,28198,29926,31401,31402,33253,34521,34680,35355,23113,23436,23451,26785,26880,28003,29609,29715,29740,30871,32233,32747,33048,33109,33694,35916,38446,38929,26352,24448,26106,26505,27754,29579,20525,23043,27498,30702,22806,23916,24013,29477,30031,63930,63931,20709,20985,22575,22829,22934,23002,23525,63932,63933,23970,25303,25622,25747,25854,63934,26332,63935,27208,63936,29183,29796,63937,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31368,31407,32327,32350,32768,33136,63938,34799,35201,35616,36953,63939,36992,39250,24958,27442,28020,32287,35109,36785,20433,20653,20887,21191,22471,22665,23481,24248,24898,27029,28044,28263,28342,29076,29794,29992,29996,32883,33592,33993,36362,37780,37854,63940,20110,20305,20598,20778,21448,21451,21491,23431,23507,23588,24858,24962,26100,29275,29591,29760,30402,31056,31121,31161,32006,32701,33419,34261,34398,36802,36935,37109,37354,38533,38632,38633,21206,24423,26093,26161,26671,29020,31286,37057,38922,20113,63941,27218,27550,28560,29065,32792,33464,34131,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36939,38549,38642,38907,34074,39729,20112,29066,38596,20803,21407,21729,22291,22290,22435,23195,23236,23491,24616,24895,25588,27781,27961,28274,28304,29232,29503,29783,33489,34945,36677,36960,63942,38498,39000,40219,26376,36234,37470,20301,20553,20702,21361,22285,22996,23041,23561,24944,26256,28205,29234,29771,32239,32963,33806,33894,34111,34655,34907,35096,35586,36949,38859,39759,20083,20369,20754,20842,63943,21807,21929,23418,23461,24188,24189,24254,24736,24799,24840,24841,25540,25912,26377,63944,26580,26586,63945,26977,26978,27833,27943,63946,28216,63947,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,28641,29494,29495,63948,29788,30001,63949,30290,63950,63951,32173,33278,33848,35029,35480,35547,35565,36400,36418,36938,36926,36986,37193,37321,37742,63952,63953,22537,63954,27603,32905,32946,63955,63956,20801,22891,23609,63957,63958,28516,29607,32996,36103,63959,37399,38287,63960,63961,63962,63963,32895,25102,28700,32104,34701,63964,22432,24681,24903,27575,35518,37504,38577,20057,21535,28139,34093,38512,38899,39150,25558,27875,37009,20957,25033,33210,40441,20381,20506,20736,23452,24847,25087,25836,26885,27589,30097,30691,32681,33380,34191,34811,34915,35516,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,35696,37291,20108,20197,20234,63965,63966,22839,23016,63967,24050,24347,24411,24609,63968,63969,63970,63971,29246,29669,63972,30064,30157,63973,31227,63974,32780,32819,32900,33505,33617,63975,63976,36029,36019,36999,63977,63978,39156,39180,63979,63980,28727,30410,32714,32716,32764,35610,20154,20161,20995,21360,63981,21693,22240,23035,23493,24341,24525,28270,63982,63983,32106,33589,63984,34451,35469,63985,38765,38775,63986,63987,19968,20314,20350,22777,26085,28322,36920,37808,39353,20219,22764,22922,23001,24641,63988,63989,31252,63990,33615,36035,20837,21316,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63991,63992,63993,20173,21097,23381,33471,20180,21050,21672,22985,23039,23376,23383,23388,24675,24904,28363,28825,29038,29574,29943,30133,30913,32043,32773,33258,33576,34071,34249,35566,36039,38604,20316,21242,22204,26027,26152,28796,28856,29237,32189,33421,37196,38592,40306,23409,26855,27544,28538,30430,23697,26283,28507,31668,31786,34870,38620,19976,20183,21280,22580,22715,22767,22892,23559,24115,24196,24373,25484,26290,26454,27167,27299,27404,28479,29254,63994,29520,29835,31456,31911,33144,33247,33255,33674,33900,34083,34196,34255,35037,36115,37292,38263,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,38556,20877,21705,22312,23472,25165,26448,26685,26771,28221,28371,28797,32289,35009,36001,36617,40779,40782,29229,31631,35533,37658,20295,20302,20786,21632,22992,24213,25269,26485,26990,27159,27822,28186,29401,29482,30141,31672,32053,33511,33785,33879,34295,35419,36015,36487,36889,37048,38606,40799,21219,21514,23265,23490,25688,25973,28404,29380,63995,30340,31309,31515,31821,32318,32735,33659,35627,36042,36196,36321,36447,36842,36857,36969,37841,20291,20346,20659,20840,20856,21069,21098,22625,22652,22880,23560,23637,24283,24731,25136,26643,27583,27656,28593,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,29006,29728,30000,30008,30033,30322,31564,31627,31661,31686,32399,35438,36670,36681,37439,37523,37666,37931,38651,39002,39019,39198,20999,25130,25240,27993,30308,31434,31680,32118,21344,23742,24215,28472,28857,31896,38673,39822,40670,25509,25722,34678,19969,20117,20141,20572,20597,21576,22979,23450,24128,24237,24311,24449,24773,25402,25919,25972,26060,26230,26232,26622,26984,27273,27491,27712,28096,28136,28191,28254,28702,28833,29582,29693,30010,30555,30855,31118,31243,31357,31934,32142,33351,35330,35562,35998,37165,37194,37336,37478,37580,37664,38662,38742,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,38748,38914,40718,21046,21137,21884,22564,24093,24351,24716,25552,26799,28639,31085,31532,33229,34234,35069,35576,36420,37261,38500,38555,38717,38988,40778,20430,20806,20939,21161,22066,24340,24427,25514,25805,26089,26177,26362,26361,26397,26781,26839,27133,28437,28526,29031,29157,29226,29866,30522,31062,31066,31199,31264,31381,31895,31967,32068,32368,32903,34299,34468,35412,35519,36249,36481,36896,36973,37347,38459,38613,40165,26063,31751,36275,37827,23384,23562,21330,25305,29469,20519,23447,24478,24752,24939,26837,28121,29742,31278,32066,32156,32305,33131,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36394,36405,37758,37912,20304,22352,24038,24231,25387,32618,20027,20303,20367,20570,23005,32964,21610,21608,22014,22863,23449,24030,24282,26205,26417,26609,26666,27880,27954,28234,28557,28855,29664,30087,31820,32002,32044,32162,33311,34523,35387,35461,36208,36490,36659,36913,37198,37202,37956,39376,31481,31909,20426,20737,20934,22472,23535,23803,26201,27197,27994,28310,28652,28940,30063,31459,34850,36897,36981,38603,39423,33537,20013,20210,34886,37325,21373,27355,26987,27713,33914,22686,24974,26366,25327,28893,29969,30151,32338,33976,35657,36104,20043,21482,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21675,22320,22336,24535,25345,25351,25711,25903,26088,26234,26525,26547,27490,27744,27802,28460,30693,30757,31049,31063,32025,32930,33026,33267,33437,33463,34584,35468,63996,36100,36286,36978,30452,31257,31287,32340,32887,21767,21972,22645,25391,25634,26185,26187,26733,27035,27524,27941,28337,29645,29800,29857,30043,30137,30433,30494,30603,31206,32265,32285,33275,34095,34967,35386,36049,36587,36784,36914,37805,38499,38515,38663,20356,21489,23018,23241,24089,26702,29894,30142,31209,31378,33187,34541,36074,36300,36845,26015,26389,63997,22519,28503,32221,36655,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,37878,38598,24501,25074,28548,19988,20376,20511,21449,21983,23919,24046,27425,27492,30923,31642,63998,36425,36554,36974,25417,25662,30528,31364,37679,38015,40810,25776,28591,29158,29864,29914,31428,31762,32386,31922,32408,35738,36106,38013,39184,39244,21049,23519,25830,26413,32046,20717,21443,22649,24920,24921,25082,26028,31449,35730,35734,20489,20513,21109,21809,23100,24288,24432,24884,25950,26124,26166,26274,27085,28356,28466,29462,30241,31379,33081,33369,33750,33980,20661,22512,23488,23528,24425,25505,30758,32181,33756,34081,37319,37365,20874,26613,31574,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36012,20932,22971,24765,34389,20508,63999,21076,23610,24957,25114,25299,25842,26021,28364,30240,33034,36448,38495,38587,20191,21315,21912,22825,24029,25797,27849,28154,29588,31359,33307,34214,36068,36368,36983,37351,38369,38433,38854,20984,21746,21894,24505,25764,28552,32180,36639,36685,37941,20681,23574,27838,28155,29979,30651,31805,31844,35449,35522,22558,22974,24086,25463,29266,30090,30571,35548,36028,36626,24307,26228,28152,32893,33729,35531,38737,39894,64000,21059,26367,28053,28399,32224,35558,36910,36958,39636,21021,21119,21736,24980,25220,25307,26786,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26898,26970,27189,28818,28966,30813,30977,30990,31186,31245,32918,33400,33493,33609,34121,35970,36229,37218,37259,37294,20419,22225,29165,30679,34560,35320,23544,24534,26449,37032,21474,22618,23541,24740,24961,25696,32317,32880,34085,37507,25774,20652,23828,26368,22684,25277,25512,26894,27000,27166,28267,30394,31179,33467,33833,35535,36264,36861,37138,37195,37276,37648,37656,37786,38619,39478,39949,19985,30044,31069,31482,31569,31689,32302,33988,36441,36468,36600,36880,26149,26943,29763,20986,26414,40668,20805,24544,27798,34802,34909,34935,24756,33205,33795,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36101,21462,21561,22068,23094,23601,28810,32736,32858,33030,33261,36259,37257,39519,40434,20596,20164,21408,24827,28204,23652,20360,20516,21988,23769,24159,24677,26772,27835,28100,29118,30164,30196,30305,31258,31305,32199,32251,32622,33268,34473,36636,38601,39347,40786,21063,21189,39149,35242,19971,26578,28422,20405,23522,26517,27784,28024,29723,30759,37341,37756,34756,31204,31281,24555,20182,21668,21822,22702,22949,24816,25171,25302,26422,26965,33333,38464,39345,39389,20524,21331,21828,22396,64001,25176,64002,25826,26219,26589,28609,28655,29730,29752,35351,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,37944,21585,22022,22374,24392,24986,27470,28760,28845,32187,35477,22890,33067,25506,30472,32829,36010,22612,25645,27067,23445,24081,28271,64003,34153,20812,21488,22826,24608,24907,27526,27760,27888,31518,32974,33492,36294,37040,39089,64004,25799,28580,25745,25860,20814,21520,22303,35342,24927,26742,64005,30171,31570,32113,36890,22534,27084,33151,35114,36864,38969,20600,22871,22956,25237,36879,39722,24925,29305,38358,22369,23110,24052,25226,25773,25850,26487,27874,27966,29228,29750,30772,32631,33453,36315,38935,21028,22338,26495,29256,29923,36009,36774,37393,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,38442,20843,21485,25420,20329,21764,24726,25943,27803,28031,29260,29437,31255,35207,35997,24429,28558,28921,33192,24846,20415,20559,25153,29255,31687,32232,32745,36941,38829,39449,36022,22378,24179,26544,33805,35413,21536,23318,24163,24290,24330,25987,32954,34109,38281,38491,20296,21253,21261,21263,21638,21754,22275,24067,24598,25243,25265,25429,64006,27873,28006,30129,30770,32990,33071,33502,33889,33970,34957,35090,36875,37610,39165,39825,24133,26292,26333,28689,29190,64007,20469,21117,24426,24915,26451,27161,28418,29922,31080,34920,35961,39111,39108,39491,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21697,31263,26963,35575,35914,39080,39342,24444,25259,30130,30382,34987,36991,38466,21305,24380,24517,27852,29644,30050,30091,31558,33534,39325,20047,36924,19979,20309,21414,22799,24264,26160,27827,29781,33655,34662,36032,36944,38686,39957,22737,23416,34384,35604,40372,23506,24680,24717,26097,27735,28450,28579,28698,32597,32752,38289,38290,38480,38867,21106,36676,20989,21547,21688,21859,21898,27323,28085,32216,33382,37532,38519,40569,21512,21704,30418,34532,38308,38356,38492,20130,20233,23022,23270,24055,24658,25239,26477,26689,27782,28207,32568,32923,33322,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,64008,64009,38917,20133,20565,21683,22419,22874,23401,23475,25032,26999,28023,28707,34809,35299,35442,35559,36994,39405,39608,21182,26680,20502,24184,26447,33607,34892,20139,21521,22190,29670,37141,38911,39177,39255,39321,22099,22687,34395,35377,25010,27382,29563,36562,27463,38570,39511,22869,29184,36203,38761,20436,23796,24358,25080,26203,27883,28843,29572,29625,29694,30505,30541,32067,32098,32291,33335,34898,64010,36066,37449,39023,23377,31348,34880,38913,23244,20448,21332,22846,23805,25406,28025,29433,33029,33031,33698,37583,38960,20136,20804,21009,22411,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,24418,27842,28366,28677,28752,28847,29074,29673,29801,33610,34722,34913,36872,37026,37795,39336,20846,24407,24800,24935,26291,34137,36426,37295,38795,20046,20114,21628,22741,22778,22909,23733,24359,25142,25160,26122,26215,27627,28009,28111,28246,28408,28564,28640,28649,28765,29392,29733,29786,29920,30355,31068,31946,32286,32993,33446,33899,33983,34382,34399,34676,35703,35946,37804,38912,39013,24785,25110,37239,23130,26127,28151,28222,29759,39746,24573,24794,31503,21700,24344,27742,27859,27946,28888,32005,34425,35340,40251,21270,21644,23301,27194,28779,30069,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31117,31166,33457,33775,35441,35649,36008,38772,64011,25844,25899,30906,30907,31339,20024,21914,22864,23462,24187,24739,25563,27489,26213,26707,28185,29029,29872,32008,36996,39529,39973,27963,28369,29502,35905,38346,20976,24140,24488,24653,24822,24880,24908,26179,26180,27045,27841,28255,28361,28514,29004,29852,30343,31681,31783,33618,34647,36945,38541,40643,21295,22238,24315,24458,24674,24724,25079,26214,26371,27292,28142,28590,28784,29546,32362,33214,33588,34516,35496,36036,21123,29554,23446,27243,37892,21742,22150,23389,25928,25989,26313,26783,28045,28102,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,29243,32948,37237,39501,20399,20505,21402,21518,21564,21897,21957,24127,24460,26429,29030,29661,36869,21211,21235,22628,22734,28932,29071,29179,34224,35347,26248,34216,21927,26244,29002,33841,21321,21913,27585,24409,24509,25582,26249,28999,35569,36637,40638,20241,25658,28875,30054,34407,24676,35662,40440,20807,20982,21256,27958,33016,40657,26133,27427,28824,30165,21507,23673,32007,35350,27424,27453,27462,21560,24688,27965,32725,33288,20694,20958,21916,22123,22221,23020,23305,24076,24985,24984,25137,26206,26342,29081,29113,29114,29351,31143,31232,32690,35440,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null], + "gb18030":[19970,19972,19973,19974,19983,19986,19991,19999,20000,20001,20003,20006,20009,20014,20015,20017,20019,20021,20023,20028,20032,20033,20034,20036,20038,20042,20049,20053,20055,20058,20059,20066,20067,20068,20069,20071,20072,20074,20075,20076,20077,20078,20079,20082,20084,20085,20086,20087,20088,20089,20090,20091,20092,20093,20095,20096,20097,20098,20099,20100,20101,20103,20106,20112,20118,20119,20121,20124,20125,20126,20131,20138,20143,20144,20145,20148,20150,20151,20152,20153,20156,20157,20158,20168,20172,20175,20176,20178,20186,20187,20188,20192,20194,20198,20199,20201,20205,20206,20207,20209,20212,20216,20217,20218,20220,20222,20224,20226,20227,20228,20229,20230,20231,20232,20235,20236,20242,20243,20244,20245,20246,20252,20253,20257,20259,20264,20265,20268,20269,20270,20273,20275,20277,20279,20281,20283,20286,20287,20288,20289,20290,20292,20293,20295,20296,20297,20298,20299,20300,20306,20308,20310,20321,20322,20326,20328,20330,20331,20333,20334,20337,20338,20341,20343,20344,20345,20346,20349,20352,20353,20354,20357,20358,20359,20362,20364,20366,20368,20370,20371,20373,20374,20376,20377,20378,20380,20382,20383,20385,20386,20388,20395,20397,20400,20401,20402,20403,20404,20406,20407,20408,20409,20410,20411,20412,20413,20414,20416,20417,20418,20422,20423,20424,20425,20427,20428,20429,20434,20435,20436,20437,20438,20441,20443,20448,20450,20452,20453,20455,20459,20460,20464,20466,20468,20469,20470,20471,20473,20475,20476,20477,20479,20480,20481,20482,20483,20484,20485,20486,20487,20488,20489,20490,20491,20494,20496,20497,20499,20501,20502,20503,20507,20509,20510,20512,20514,20515,20516,20519,20523,20527,20528,20529,20530,20531,20532,20533,20534,20535,20536,20537,20539,20541,20543,20544,20545,20546,20548,20549,20550,20553,20554,20555,20557,20560,20561,20562,20563,20564,20566,20567,20568,20569,20571,20573,20574,20575,20576,20577,20578,20579,20580,20582,20583,20584,20585,20586,20587,20589,20590,20591,20592,20593,20594,20595,20596,20597,20600,20601,20602,20604,20605,20609,20610,20611,20612,20614,20615,20617,20618,20619,20620,20622,20623,20624,20625,20626,20627,20628,20629,20630,20631,20632,20633,20634,20635,20636,20637,20638,20639,20640,20641,20642,20644,20646,20650,20651,20653,20654,20655,20656,20657,20659,20660,20661,20662,20663,20664,20665,20668,20669,20670,20671,20672,20673,20674,20675,20676,20677,20678,20679,20680,20681,20682,20683,20684,20685,20686,20688,20689,20690,20691,20692,20693,20695,20696,20697,20699,20700,20701,20702,20703,20704,20705,20706,20707,20708,20709,20712,20713,20714,20715,20719,20720,20721,20722,20724,20726,20727,20728,20729,20730,20732,20733,20734,20735,20736,20737,20738,20739,20740,20741,20744,20745,20746,20748,20749,20750,20751,20752,20753,20755,20756,20757,20758,20759,20760,20761,20762,20763,20764,20765,20766,20767,20768,20770,20771,20772,20773,20774,20775,20776,20777,20778,20779,20780,20781,20782,20783,20784,20785,20786,20787,20788,20789,20790,20791,20792,20793,20794,20795,20796,20797,20798,20802,20807,20810,20812,20814,20815,20816,20818,20819,20823,20824,20825,20827,20829,20830,20831,20832,20833,20835,20836,20838,20839,20841,20842,20847,20850,20858,20862,20863,20867,20868,20870,20871,20874,20875,20878,20879,20880,20881,20883,20884,20888,20890,20893,20894,20895,20897,20899,20902,20903,20904,20905,20906,20909,20910,20916,20920,20921,20922,20926,20927,20929,20930,20931,20933,20936,20938,20941,20942,20944,20946,20947,20948,20949,20950,20951,20952,20953,20954,20956,20958,20959,20962,20963,20965,20966,20967,20968,20969,20970,20972,20974,20977,20978,20980,20983,20990,20996,20997,21001,21003,21004,21007,21008,21011,21012,21013,21020,21022,21023,21025,21026,21027,21029,21030,21031,21034,21036,21039,21041,21042,21044,21045,21052,21054,21060,21061,21062,21063,21064,21065,21067,21070,21071,21074,21075,21077,21079,21080,21081,21082,21083,21085,21087,21088,21090,21091,21092,21094,21096,21099,21100,21101,21102,21104,21105,21107,21108,21109,21110,21111,21112,21113,21114,21115,21116,21118,21120,21123,21124,21125,21126,21127,21129,21130,21131,21132,21133,21134,21135,21137,21138,21140,21141,21142,21143,21144,21145,21146,21148,21156,21157,21158,21159,21166,21167,21168,21172,21173,21174,21175,21176,21177,21178,21179,21180,21181,21184,21185,21186,21188,21189,21190,21192,21194,21196,21197,21198,21199,21201,21203,21204,21205,21207,21209,21210,21211,21212,21213,21214,21216,21217,21218,21219,21221,21222,21223,21224,21225,21226,21227,21228,21229,21230,21231,21233,21234,21235,21236,21237,21238,21239,21240,21243,21244,21245,21249,21250,21251,21252,21255,21257,21258,21259,21260,21262,21265,21266,21267,21268,21272,21275,21276,21278,21279,21282,21284,21285,21287,21288,21289,21291,21292,21293,21295,21296,21297,21298,21299,21300,21301,21302,21303,21304,21308,21309,21312,21314,21316,21318,21323,21324,21325,21328,21332,21336,21337,21339,21341,21349,21352,21354,21356,21357,21362,21366,21369,21371,21372,21373,21374,21376,21377,21379,21383,21384,21386,21390,21391,21392,21393,21394,21395,21396,21398,21399,21401,21403,21404,21406,21408,21409,21412,21415,21418,21419,21420,21421,21423,21424,21425,21426,21427,21428,21429,21431,21432,21433,21434,21436,21437,21438,21440,21443,21444,21445,21446,21447,21454,21455,21456,21458,21459,21461,21466,21468,21469,21470,21473,21474,21479,21492,21498,21502,21503,21504,21506,21509,21511,21515,21524,21528,21529,21530,21532,21538,21540,21541,21546,21552,21555,21558,21559,21562,21565,21567,21569,21570,21572,21573,21575,21577,21580,21581,21582,21583,21585,21594,21597,21598,21599,21600,21601,21603,21605,21607,21609,21610,21611,21612,21613,21614,21615,21616,21620,21625,21626,21630,21631,21633,21635,21637,21639,21640,21641,21642,21645,21649,21651,21655,21656,21660,21662,21663,21664,21665,21666,21669,21678,21680,21682,21685,21686,21687,21689,21690,21692,21694,21699,21701,21706,21707,21718,21720,21723,21728,21729,21730,21731,21732,21739,21740,21743,21744,21745,21748,21749,21750,21751,21752,21753,21755,21758,21760,21762,21763,21764,21765,21768,21770,21771,21772,21773,21774,21778,21779,21781,21782,21783,21784,21785,21786,21788,21789,21790,21791,21793,21797,21798,21800,21801,21803,21805,21810,21812,21813,21814,21816,21817,21818,21819,21821,21824,21826,21829,21831,21832,21835,21836,21837,21838,21839,21841,21842,21843,21844,21847,21848,21849,21850,21851,21853,21854,21855,21856,21858,21859,21864,21865,21867,21871,21872,21873,21874,21875,21876,21881,21882,21885,21887,21893,21894,21900,21901,21902,21904,21906,21907,21909,21910,21911,21914,21915,21918,21920,21921,21922,21923,21924,21925,21926,21928,21929,21930,21931,21932,21933,21934,21935,21936,21938,21940,21942,21944,21946,21948,21951,21952,21953,21954,21955,21958,21959,21960,21962,21963,21966,21967,21968,21973,21975,21976,21977,21978,21979,21982,21984,21986,21991,21993,21997,21998,22000,22001,22004,22006,22008,22009,22010,22011,22012,22015,22018,22019,22020,22021,22022,22023,22026,22027,22029,22032,22033,22034,22035,22036,22037,22038,22039,22041,22042,22044,22045,22048,22049,22050,22053,22054,22056,22057,22058,22059,22062,22063,22064,22067,22069,22071,22072,22074,22076,22077,22078,22080,22081,22082,22083,22084,22085,22086,22087,22088,22089,22090,22091,22095,22096,22097,22098,22099,22101,22102,22106,22107,22109,22110,22111,22112,22113,22115,22117,22118,22119,22125,22126,22127,22128,22130,22131,22132,22133,22135,22136,22137,22138,22141,22142,22143,22144,22145,22146,22147,22148,22151,22152,22153,22154,22155,22156,22157,22160,22161,22162,22164,22165,22166,22167,22168,22169,22170,22171,22172,22173,22174,22175,22176,22177,22178,22180,22181,22182,22183,22184,22185,22186,22187,22188,22189,22190,22192,22193,22194,22195,22196,22197,22198,22200,22201,22202,22203,22205,22206,22207,22208,22209,22210,22211,22212,22213,22214,22215,22216,22217,22219,22220,22221,22222,22223,22224,22225,22226,22227,22229,22230,22232,22233,22236,22243,22245,22246,22247,22248,22249,22250,22252,22254,22255,22258,22259,22262,22263,22264,22267,22268,22272,22273,22274,22277,22279,22283,22284,22285,22286,22287,22288,22289,22290,22291,22292,22293,22294,22295,22296,22297,22298,22299,22301,22302,22304,22305,22306,22308,22309,22310,22311,22315,22321,22322,22324,22325,22326,22327,22328,22332,22333,22335,22337,22339,22340,22341,22342,22344,22345,22347,22354,22355,22356,22357,22358,22360,22361,22370,22371,22373,22375,22380,22382,22384,22385,22386,22388,22389,22392,22393,22394,22397,22398,22399,22400,22401,22407,22408,22409,22410,22413,22414,22415,22416,22417,22420,22421,22422,22423,22424,22425,22426,22428,22429,22430,22431,22437,22440,22442,22444,22447,22448,22449,22451,22453,22454,22455,22457,22458,22459,22460,22461,22462,22463,22464,22465,22468,22469,22470,22471,22472,22473,22474,22476,22477,22480,22481,22483,22486,22487,22491,22492,22494,22497,22498,22499,22501,22502,22503,22504,22505,22506,22507,22508,22510,22512,22513,22514,22515,22517,22518,22519,22523,22524,22526,22527,22529,22531,22532,22533,22536,22537,22538,22540,22542,22543,22544,22546,22547,22548,22550,22551,22552,22554,22555,22556,22557,22559,22562,22563,22565,22566,22567,22568,22569,22571,22572,22573,22574,22575,22577,22578,22579,22580,22582,22583,22584,22585,22586,22587,22588,22589,22590,22591,22592,22593,22594,22595,22597,22598,22599,22600,22601,22602,22603,22606,22607,22608,22610,22611,22613,22614,22615,22617,22618,22619,22620,22621,22623,22624,22625,22626,22627,22628,22630,22631,22632,22633,22634,22637,22638,22639,22640,22641,22642,22643,22644,22645,22646,22647,22648,22649,22650,22651,22652,22653,22655,22658,22660,22662,22663,22664,22666,22667,22668,22669,22670,22671,22672,22673,22676,22677,22678,22679,22680,22683,22684,22685,22688,22689,22690,22691,22692,22693,22694,22695,22698,22699,22700,22701,22702,22703,22704,22705,22706,22707,22708,22709,22710,22711,22712,22713,22714,22715,22717,22718,22719,22720,22722,22723,22724,22726,22727,22728,22729,22730,22731,22732,22733,22734,22735,22736,22738,22739,22740,22742,22743,22744,22745,22746,22747,22748,22749,22750,22751,22752,22753,22754,22755,22757,22758,22759,22760,22761,22762,22765,22767,22769,22770,22772,22773,22775,22776,22778,22779,22780,22781,22782,22783,22784,22785,22787,22789,22790,22792,22793,22794,22795,22796,22798,22800,22801,22802,22803,22807,22808,22811,22813,22814,22816,22817,22818,22819,22822,22824,22828,22832,22834,22835,22837,22838,22843,22845,22846,22847,22848,22851,22853,22854,22858,22860,22861,22864,22866,22867,22873,22875,22876,22877,22878,22879,22881,22883,22884,22886,22887,22888,22889,22890,22891,22892,22893,22894,22895,22896,22897,22898,22901,22903,22906,22907,22908,22910,22911,22912,22917,22921,22923,22924,22926,22927,22928,22929,22932,22933,22936,22938,22939,22940,22941,22943,22944,22945,22946,22950,22951,22956,22957,22960,22961,22963,22964,22965,22966,22967,22968,22970,22972,22973,22975,22976,22977,22978,22979,22980,22981,22983,22984,22985,22988,22989,22990,22991,22997,22998,23001,23003,23006,23007,23008,23009,23010,23012,23014,23015,23017,23018,23019,23021,23022,23023,23024,23025,23026,23027,23028,23029,23030,23031,23032,23034,23036,23037,23038,23040,23042,23050,23051,23053,23054,23055,23056,23058,23060,23061,23062,23063,23065,23066,23067,23069,23070,23073,23074,23076,23078,23079,23080,23082,23083,23084,23085,23086,23087,23088,23091,23093,23095,23096,23097,23098,23099,23101,23102,23103,23105,23106,23107,23108,23109,23111,23112,23115,23116,23117,23118,23119,23120,23121,23122,23123,23124,23126,23127,23128,23129,23131,23132,23133,23134,23135,23136,23137,23139,23140,23141,23142,23144,23145,23147,23148,23149,23150,23151,23152,23153,23154,23155,23160,23161,23163,23164,23165,23166,23168,23169,23170,23171,23172,23173,23174,23175,23176,23177,23178,23179,23180,23181,23182,23183,23184,23185,23187,23188,23189,23190,23191,23192,23193,23196,23197,23198,23199,23200,23201,23202,23203,23204,23205,23206,23207,23208,23209,23211,23212,23213,23214,23215,23216,23217,23220,23222,23223,23225,23226,23227,23228,23229,23231,23232,23235,23236,23237,23238,23239,23240,23242,23243,23245,23246,23247,23248,23249,23251,23253,23255,23257,23258,23259,23261,23262,23263,23266,23268,23269,23271,23272,23274,23276,23277,23278,23279,23280,23282,23283,23284,23285,23286,23287,23288,23289,23290,23291,23292,23293,23294,23295,23296,23297,23298,23299,23300,23301,23302,23303,23304,23306,23307,23308,23309,23310,23311,23312,23313,23314,23315,23316,23317,23320,23321,23322,23323,23324,23325,23326,23327,23328,23329,23330,23331,23332,23333,23334,23335,23336,23337,23338,23339,23340,23341,23342,23343,23344,23345,23347,23349,23350,23352,23353,23354,23355,23356,23357,23358,23359,23361,23362,23363,23364,23365,23366,23367,23368,23369,23370,23371,23372,23373,23374,23375,23378,23382,23390,23392,23393,23399,23400,23403,23405,23406,23407,23410,23412,23414,23415,23416,23417,23419,23420,23422,23423,23426,23430,23434,23437,23438,23440,23441,23442,23444,23446,23455,23463,23464,23465,23468,23469,23470,23471,23473,23474,23479,23482,23483,23484,23488,23489,23491,23496,23497,23498,23499,23501,23502,23503,23505,23508,23509,23510,23511,23512,23513,23514,23515,23516,23520,23522,23523,23526,23527,23529,23530,23531,23532,23533,23535,23537,23538,23539,23540,23541,23542,23543,23549,23550,23552,23554,23555,23557,23559,23560,23563,23564,23565,23566,23568,23570,23571,23575,23577,23579,23582,23583,23584,23585,23587,23590,23592,23593,23594,23595,23597,23598,23599,23600,23602,23603,23605,23606,23607,23619,23620,23622,23623,23628,23629,23634,23635,23636,23638,23639,23640,23642,23643,23644,23645,23647,23650,23652,23655,23656,23657,23658,23659,23660,23661,23664,23666,23667,23668,23669,23670,23671,23672,23675,23676,23677,23678,23680,23683,23684,23685,23686,23687,23689,23690,23691,23694,23695,23698,23699,23701,23709,23710,23711,23712,23713,23716,23717,23718,23719,23720,23722,23726,23727,23728,23730,23732,23734,23737,23738,23739,23740,23742,23744,23746,23747,23749,23750,23751,23752,23753,23754,23756,23757,23758,23759,23760,23761,23763,23764,23765,23766,23767,23768,23770,23771,23772,23773,23774,23775,23776,23778,23779,23783,23785,23787,23788,23790,23791,23793,23794,23795,23796,23797,23798,23799,23800,23801,23802,23804,23805,23806,23807,23808,23809,23812,23813,23816,23817,23818,23819,23820,23821,23823,23824,23825,23826,23827,23829,23831,23832,23833,23834,23836,23837,23839,23840,23841,23842,23843,23845,23848,23850,23851,23852,23855,23856,23857,23858,23859,23861,23862,23863,23864,23865,23866,23867,23868,23871,23872,23873,23874,23875,23876,23877,23878,23880,23881,23885,23886,23887,23888,23889,23890,23891,23892,23893,23894,23895,23897,23898,23900,23902,23903,23904,23905,23906,23907,23908,23909,23910,23911,23912,23914,23917,23918,23920,23921,23922,23923,23925,23926,23927,23928,23929,23930,23931,23932,23933,23934,23935,23936,23937,23939,23940,23941,23942,23943,23944,23945,23946,23947,23948,23949,23950,23951,23952,23953,23954,23955,23956,23957,23958,23959,23960,23962,23963,23964,23966,23967,23968,23969,23970,23971,23972,23973,23974,23975,23976,23977,23978,23979,23980,23981,23982,23983,23984,23985,23986,23987,23988,23989,23990,23992,23993,23994,23995,23996,23997,23998,23999,24000,24001,24002,24003,24004,24006,24007,24008,24009,24010,24011,24012,24014,24015,24016,24017,24018,24019,24020,24021,24022,24023,24024,24025,24026,24028,24031,24032,24035,24036,24042,24044,24045,24048,24053,24054,24056,24057,24058,24059,24060,24063,24064,24068,24071,24073,24074,24075,24077,24078,24082,24083,24087,24094,24095,24096,24097,24098,24099,24100,24101,24104,24105,24106,24107,24108,24111,24112,24114,24115,24116,24117,24118,24121,24122,24126,24127,24128,24129,24131,24134,24135,24136,24137,24138,24139,24141,24142,24143,24144,24145,24146,24147,24150,24151,24152,24153,24154,24156,24157,24159,24160,24163,24164,24165,24166,24167,24168,24169,24170,24171,24172,24173,24174,24175,24176,24177,24181,24183,24185,24190,24193,24194,24195,24197,24200,24201,24204,24205,24206,24210,24216,24219,24221,24225,24226,24227,24228,24232,24233,24234,24235,24236,24238,24239,24240,24241,24242,24244,24250,24251,24252,24253,24255,24256,24257,24258,24259,24260,24261,24262,24263,24264,24267,24268,24269,24270,24271,24272,24276,24277,24279,24280,24281,24282,24284,24285,24286,24287,24288,24289,24290,24291,24292,24293,24294,24295,24297,24299,24300,24301,24302,24303,24304,24305,24306,24307,24309,24312,24313,24315,24316,24317,24325,24326,24327,24329,24332,24333,24334,24336,24338,24340,24342,24345,24346,24348,24349,24350,24353,24354,24355,24356,24360,24363,24364,24366,24368,24370,24371,24372,24373,24374,24375,24376,24379,24381,24382,24383,24385,24386,24387,24388,24389,24390,24391,24392,24393,24394,24395,24396,24397,24398,24399,24401,24404,24409,24410,24411,24412,24414,24415,24416,24419,24421,24423,24424,24427,24430,24431,24434,24436,24437,24438,24440,24442,24445,24446,24447,24451,24454,24461,24462,24463,24465,24467,24468,24470,24474,24475,24477,24478,24479,24480,24482,24483,24484,24485,24486,24487,24489,24491,24492,24495,24496,24497,24498,24499,24500,24502,24504,24505,24506,24507,24510,24511,24512,24513,24514,24519,24520,24522,24523,24526,24531,24532,24533,24538,24539,24540,24542,24543,24546,24547,24549,24550,24552,24553,24556,24559,24560,24562,24563,24564,24566,24567,24569,24570,24572,24583,24584,24585,24587,24588,24592,24593,24595,24599,24600,24602,24606,24607,24610,24611,24612,24620,24621,24622,24624,24625,24626,24627,24628,24630,24631,24632,24633,24634,24637,24638,24640,24644,24645,24646,24647,24648,24649,24650,24652,24654,24655,24657,24659,24660,24662,24663,24664,24667,24668,24670,24671,24672,24673,24677,24678,24686,24689,24690,24692,24693,24695,24702,24704,24705,24706,24709,24710,24711,24712,24714,24715,24718,24719,24720,24721,24723,24725,24727,24728,24729,24732,24734,24737,24738,24740,24741,24743,24745,24746,24750,24752,24755,24757,24758,24759,24761,24762,24765,24766,24767,24768,24769,24770,24771,24772,24775,24776,24777,24780,24781,24782,24783,24784,24786,24787,24788,24790,24791,24793,24795,24798,24801,24802,24803,24804,24805,24810,24817,24818,24821,24823,24824,24827,24828,24829,24830,24831,24834,24835,24836,24837,24839,24842,24843,24844,24848,24849,24850,24851,24852,24854,24855,24856,24857,24859,24860,24861,24862,24865,24866,24869,24872,24873,24874,24876,24877,24878,24879,24880,24881,24882,24883,24884,24885,24886,24887,24888,24889,24890,24891,24892,24893,24894,24896,24897,24898,24899,24900,24901,24902,24903,24905,24907,24909,24911,24912,24914,24915,24916,24918,24919,24920,24921,24922,24923,24924,24926,24927,24928,24929,24931,24932,24933,24934,24937,24938,24939,24940,24941,24942,24943,24945,24946,24947,24948,24950,24952,24953,24954,24955,24956,24957,24958,24959,24960,24961,24962,24963,24964,24965,24966,24967,24968,24969,24970,24972,24973,24975,24976,24977,24978,24979,24981,24982,24983,24984,24985,24986,24987,24988,24990,24991,24992,24993,24994,24995,24996,24997,24998,25002,25003,25005,25006,25007,25008,25009,25010,25011,25012,25013,25014,25016,25017,25018,25019,25020,25021,25023,25024,25025,25027,25028,25029,25030,25031,25033,25036,25037,25038,25039,25040,25043,25045,25046,25047,25048,25049,25050,25051,25052,25053,25054,25055,25056,25057,25058,25059,25060,25061,25063,25064,25065,25066,25067,25068,25069,25070,25071,25072,25073,25074,25075,25076,25078,25079,25080,25081,25082,25083,25084,25085,25086,25088,25089,25090,25091,25092,25093,25095,25097,25107,25108,25113,25116,25117,25118,25120,25123,25126,25127,25128,25129,25131,25133,25135,25136,25137,25138,25141,25142,25144,25145,25146,25147,25148,25154,25156,25157,25158,25162,25167,25168,25173,25174,25175,25177,25178,25180,25181,25182,25183,25184,25185,25186,25188,25189,25192,25201,25202,25204,25205,25207,25208,25210,25211,25213,25217,25218,25219,25221,25222,25223,25224,25227,25228,25229,25230,25231,25232,25236,25241,25244,25245,25246,25251,25254,25255,25257,25258,25261,25262,25263,25264,25266,25267,25268,25270,25271,25272,25274,25278,25280,25281,25283,25291,25295,25297,25301,25309,25310,25312,25313,25316,25322,25323,25328,25330,25333,25336,25337,25338,25339,25344,25347,25348,25349,25350,25354,25355,25356,25357,25359,25360,25362,25363,25364,25365,25367,25368,25369,25372,25382,25383,25385,25388,25389,25390,25392,25393,25395,25396,25397,25398,25399,25400,25403,25404,25406,25407,25408,25409,25412,25415,25416,25418,25425,25426,25427,25428,25430,25431,25432,25433,25434,25435,25436,25437,25440,25444,25445,25446,25448,25450,25451,25452,25455,25456,25458,25459,25460,25461,25464,25465,25468,25469,25470,25471,25473,25475,25476,25477,25478,25483,25485,25489,25491,25492,25493,25495,25497,25498,25499,25500,25501,25502,25503,25505,25508,25510,25515,25519,25521,25522,25525,25526,25529,25531,25533,25535,25536,25537,25538,25539,25541,25543,25544,25546,25547,25548,25553,25555,25556,25557,25559,25560,25561,25562,25563,25564,25565,25567,25570,25572,25573,25574,25575,25576,25579,25580,25582,25583,25584,25585,25587,25589,25591,25593,25594,25595,25596,25598,25603,25604,25606,25607,25608,25609,25610,25613,25614,25617,25618,25621,25622,25623,25624,25625,25626,25629,25631,25634,25635,25636,25637,25639,25640,25641,25643,25646,25647,25648,25649,25650,25651,25653,25654,25655,25656,25657,25659,25660,25662,25664,25666,25667,25673,25675,25676,25677,25678,25679,25680,25681,25683,25685,25686,25687,25689,25690,25691,25692,25693,25695,25696,25697,25698,25699,25700,25701,25702,25704,25706,25707,25708,25710,25711,25712,25713,25714,25715,25716,25717,25718,25719,25723,25724,25725,25726,25727,25728,25729,25731,25734,25736,25737,25738,25739,25740,25741,25742,25743,25744,25747,25748,25751,25752,25754,25755,25756,25757,25759,25760,25761,25762,25763,25765,25766,25767,25768,25770,25771,25775,25777,25778,25779,25780,25782,25785,25787,25789,25790,25791,25793,25795,25796,25798,25799,25800,25801,25802,25803,25804,25807,25809,25811,25812,25813,25814,25817,25818,25819,25820,25821,25823,25824,25825,25827,25829,25831,25832,25833,25834,25835,25836,25837,25838,25839,25840,25841,25842,25843,25844,25845,25846,25847,25848,25849,25850,25851,25852,25853,25854,25855,25857,25858,25859,25860,25861,25862,25863,25864,25866,25867,25868,25869,25870,25871,25872,25873,25875,25876,25877,25878,25879,25881,25882,25883,25884,25885,25886,25887,25888,25889,25890,25891,25892,25894,25895,25896,25897,25898,25900,25901,25904,25905,25906,25907,25911,25914,25916,25917,25920,25921,25922,25923,25924,25926,25927,25930,25931,25933,25934,25936,25938,25939,25940,25943,25944,25946,25948,25951,25952,25953,25956,25957,25959,25960,25961,25962,25965,25966,25967,25969,25971,25973,25974,25976,25977,25978,25979,25980,25981,25982,25983,25984,25985,25986,25987,25988,25989,25990,25992,25993,25994,25997,25998,25999,26002,26004,26005,26006,26008,26010,26013,26014,26016,26018,26019,26022,26024,26026,26028,26030,26033,26034,26035,26036,26037,26038,26039,26040,26042,26043,26046,26047,26048,26050,26055,26056,26057,26058,26061,26064,26065,26067,26068,26069,26072,26073,26074,26075,26076,26077,26078,26079,26081,26083,26084,26090,26091,26098,26099,26100,26101,26104,26105,26107,26108,26109,26110,26111,26113,26116,26117,26119,26120,26121,26123,26125,26128,26129,26130,26134,26135,26136,26138,26139,26140,26142,26145,26146,26147,26148,26150,26153,26154,26155,26156,26158,26160,26162,26163,26167,26168,26169,26170,26171,26173,26175,26176,26178,26180,26181,26182,26183,26184,26185,26186,26189,26190,26192,26193,26200,26201,26203,26204,26205,26206,26208,26210,26211,26213,26215,26217,26218,26219,26220,26221,26225,26226,26227,26229,26232,26233,26235,26236,26237,26239,26240,26241,26243,26245,26246,26248,26249,26250,26251,26253,26254,26255,26256,26258,26259,26260,26261,26264,26265,26266,26267,26268,26270,26271,26272,26273,26274,26275,26276,26277,26278,26281,26282,26283,26284,26285,26287,26288,26289,26290,26291,26293,26294,26295,26296,26298,26299,26300,26301,26303,26304,26305,26306,26307,26308,26309,26310,26311,26312,26313,26314,26315,26316,26317,26318,26319,26320,26321,26322,26323,26324,26325,26326,26327,26328,26330,26334,26335,26336,26337,26338,26339,26340,26341,26343,26344,26346,26347,26348,26349,26350,26351,26353,26357,26358,26360,26362,26363,26365,26369,26370,26371,26372,26373,26374,26375,26380,26382,26383,26385,26386,26387,26390,26392,26393,26394,26396,26398,26400,26401,26402,26403,26404,26405,26407,26409,26414,26416,26418,26419,26422,26423,26424,26425,26427,26428,26430,26431,26433,26436,26437,26439,26442,26443,26445,26450,26452,26453,26455,26456,26457,26458,26459,26461,26466,26467,26468,26470,26471,26475,26476,26478,26481,26484,26486,26488,26489,26490,26491,26493,26496,26498,26499,26501,26502,26504,26506,26508,26509,26510,26511,26513,26514,26515,26516,26518,26521,26523,26527,26528,26529,26532,26534,26537,26540,26542,26545,26546,26548,26553,26554,26555,26556,26557,26558,26559,26560,26562,26565,26566,26567,26568,26569,26570,26571,26572,26573,26574,26581,26582,26583,26587,26591,26593,26595,26596,26598,26599,26600,26602,26603,26605,26606,26610,26613,26614,26615,26616,26617,26618,26619,26620,26622,26625,26626,26627,26628,26630,26637,26640,26642,26644,26645,26648,26649,26650,26651,26652,26654,26655,26656,26658,26659,26660,26661,26662,26663,26664,26667,26668,26669,26670,26671,26672,26673,26676,26677,26678,26682,26683,26687,26695,26699,26701,26703,26706,26710,26711,26712,26713,26714,26715,26716,26717,26718,26719,26730,26732,26733,26734,26735,26736,26737,26738,26739,26741,26744,26745,26746,26747,26748,26749,26750,26751,26752,26754,26756,26759,26760,26761,26762,26763,26764,26765,26766,26768,26769,26770,26772,26773,26774,26776,26777,26778,26779,26780,26781,26782,26783,26784,26785,26787,26788,26789,26793,26794,26795,26796,26798,26801,26802,26804,26806,26807,26808,26809,26810,26811,26812,26813,26814,26815,26817,26819,26820,26821,26822,26823,26824,26826,26828,26830,26831,26832,26833,26835,26836,26838,26839,26841,26843,26844,26845,26846,26847,26849,26850,26852,26853,26854,26855,26856,26857,26858,26859,26860,26861,26863,26866,26867,26868,26870,26871,26872,26875,26877,26878,26879,26880,26882,26883,26884,26886,26887,26888,26889,26890,26892,26895,26897,26899,26900,26901,26902,26903,26904,26905,26906,26907,26908,26909,26910,26913,26914,26915,26917,26918,26919,26920,26921,26922,26923,26924,26926,26927,26929,26930,26931,26933,26934,26935,26936,26938,26939,26940,26942,26944,26945,26947,26948,26949,26950,26951,26952,26953,26954,26955,26956,26957,26958,26959,26960,26961,26962,26963,26965,26966,26968,26969,26971,26972,26975,26977,26978,26980,26981,26983,26984,26985,26986,26988,26989,26991,26992,26994,26995,26996,26997,26998,27002,27003,27005,27006,27007,27009,27011,27013,27018,27019,27020,27022,27023,27024,27025,27026,27027,27030,27031,27033,27034,27037,27038,27039,27040,27041,27042,27043,27044,27045,27046,27049,27050,27052,27054,27055,27056,27058,27059,27061,27062,27064,27065,27066,27068,27069,27070,27071,27072,27074,27075,27076,27077,27078,27079,27080,27081,27083,27085,27087,27089,27090,27091,27093,27094,27095,27096,27097,27098,27100,27101,27102,27105,27106,27107,27108,27109,27110,27111,27112,27113,27114,27115,27116,27118,27119,27120,27121,27123,27124,27125,27126,27127,27128,27129,27130,27131,27132,27134,27136,27137,27138,27139,27140,27141,27142,27143,27144,27145,27147,27148,27149,27150,27151,27152,27153,27154,27155,27156,27157,27158,27161,27162,27163,27164,27165,27166,27168,27170,27171,27172,27173,27174,27175,27177,27179,27180,27181,27182,27184,27186,27187,27188,27190,27191,27192,27193,27194,27195,27196,27199,27200,27201,27202,27203,27205,27206,27208,27209,27210,27211,27212,27213,27214,27215,27217,27218,27219,27220,27221,27222,27223,27226,27228,27229,27230,27231,27232,27234,27235,27236,27238,27239,27240,27241,27242,27243,27244,27245,27246,27247,27248,27250,27251,27252,27253,27254,27255,27256,27258,27259,27261,27262,27263,27265,27266,27267,27269,27270,27271,27272,27273,27274,27275,27276,27277,27279,27282,27283,27284,27285,27286,27288,27289,27290,27291,27292,27293,27294,27295,27297,27298,27299,27300,27301,27302,27303,27304,27306,27309,27310,27311,27312,27313,27314,27315,27316,27317,27318,27319,27320,27321,27322,27323,27324,27325,27326,27327,27328,27329,27330,27331,27332,27333,27334,27335,27336,27337,27338,27339,27340,27341,27342,27343,27344,27345,27346,27347,27348,27349,27350,27351,27352,27353,27354,27355,27356,27357,27358,27359,27360,27361,27362,27363,27364,27365,27366,27367,27368,27369,27370,27371,27372,27373,27374,27375,27376,27377,27378,27379,27380,27381,27382,27383,27384,27385,27386,27387,27388,27389,27390,27391,27392,27393,27394,27395,27396,27397,27398,27399,27400,27401,27402,27403,27404,27405,27406,27407,27408,27409,27410,27411,27412,27413,27414,27415,27416,27417,27418,27419,27420,27421,27422,27423,27429,27430,27432,27433,27434,27435,27436,27437,27438,27439,27440,27441,27443,27444,27445,27446,27448,27451,27452,27453,27455,27456,27457,27458,27460,27461,27464,27466,27467,27469,27470,27471,27472,27473,27474,27475,27476,27477,27478,27479,27480,27482,27483,27484,27485,27486,27487,27488,27489,27496,27497,27499,27500,27501,27502,27503,27504,27505,27506,27507,27508,27509,27510,27511,27512,27514,27517,27518,27519,27520,27525,27528,27532,27534,27535,27536,27537,27540,27541,27543,27544,27545,27548,27549,27550,27551,27552,27554,27555,27556,27557,27558,27559,27560,27561,27563,27564,27565,27566,27567,27568,27569,27570,27574,27576,27577,27578,27579,27580,27581,27582,27584,27587,27588,27590,27591,27592,27593,27594,27596,27598,27600,27601,27608,27610,27612,27613,27614,27615,27616,27618,27619,27620,27621,27622,27623,27624,27625,27628,27629,27630,27632,27633,27634,27636,27638,27639,27640,27642,27643,27644,27646,27647,27648,27649,27650,27651,27652,27656,27657,27658,27659,27660,27662,27666,27671,27676,27677,27678,27680,27683,27685,27691,27692,27693,27697,27699,27702,27703,27705,27706,27707,27708,27710,27711,27715,27716,27717,27720,27723,27724,27725,27726,27727,27729,27730,27731,27734,27736,27737,27738,27746,27747,27749,27750,27751,27755,27756,27757,27758,27759,27761,27763,27765,27767,27768,27770,27771,27772,27775,27776,27780,27783,27786,27787,27789,27790,27793,27794,27797,27798,27799,27800,27802,27804,27805,27806,27808,27810,27816,27820,27823,27824,27828,27829,27830,27831,27834,27840,27841,27842,27843,27846,27847,27848,27851,27853,27854,27855,27857,27858,27864,27865,27866,27868,27869,27871,27876,27878,27879,27881,27884,27885,27890,27892,27897,27903,27904,27906,27907,27909,27910,27912,27913,27914,27917,27919,27920,27921,27923,27924,27925,27926,27928,27932,27933,27935,27936,27937,27938,27939,27940,27942,27944,27945,27948,27949,27951,27952,27956,27958,27959,27960,27962,27967,27968,27970,27972,27977,27980,27984,27989,27990,27991,27992,27995,27997,27999,28001,28002,28004,28005,28007,28008,28011,28012,28013,28016,28017,28018,28019,28021,28022,28025,28026,28027,28029,28030,28031,28032,28033,28035,28036,28038,28039,28042,28043,28045,28047,28048,28050,28054,28055,28056,28057,28058,28060,28066,28069,28076,28077,28080,28081,28083,28084,28086,28087,28089,28090,28091,28092,28093,28094,28097,28098,28099,28104,28105,28106,28109,28110,28111,28112,28114,28115,28116,28117,28119,28122,28123,28124,28127,28130,28131,28133,28135,28136,28137,28138,28141,28143,28144,28146,28148,28149,28150,28152,28154,28157,28158,28159,28160,28161,28162,28163,28164,28166,28167,28168,28169,28171,28175,28178,28179,28181,28184,28185,28187,28188,28190,28191,28194,28198,28199,28200,28202,28204,28206,28208,28209,28211,28213,28214,28215,28217,28219,28220,28221,28222,28223,28224,28225,28226,28229,28230,28231,28232,28233,28234,28235,28236,28239,28240,28241,28242,28245,28247,28249,28250,28252,28253,28254,28256,28257,28258,28259,28260,28261,28262,28263,28264,28265,28266,28268,28269,28271,28272,28273,28274,28275,28276,28277,28278,28279,28280,28281,28282,28283,28284,28285,28288,28289,28290,28292,28295,28296,28298,28299,28300,28301,28302,28305,28306,28307,28308,28309,28310,28311,28313,28314,28315,28317,28318,28320,28321,28323,28324,28326,28328,28329,28331,28332,28333,28334,28336,28339,28341,28344,28345,28348,28350,28351,28352,28355,28356,28357,28358,28360,28361,28362,28364,28365,28366,28368,28370,28374,28376,28377,28379,28380,28381,28387,28391,28394,28395,28396,28397,28398,28399,28400,28401,28402,28403,28405,28406,28407,28408,28410,28411,28412,28413,28414,28415,28416,28417,28419,28420,28421,28423,28424,28426,28427,28428,28429,28430,28432,28433,28434,28438,28439,28440,28441,28442,28443,28444,28445,28446,28447,28449,28450,28451,28453,28454,28455,28456,28460,28462,28464,28466,28468,28469,28471,28472,28473,28474,28475,28476,28477,28479,28480,28481,28482,28483,28484,28485,28488,28489,28490,28492,28494,28495,28496,28497,28498,28499,28500,28501,28502,28503,28505,28506,28507,28509,28511,28512,28513,28515,28516,28517,28519,28520,28521,28522,28523,28524,28527,28528,28529,28531,28533,28534,28535,28537,28539,28541,28542,28543,28544,28545,28546,28547,28549,28550,28551,28554,28555,28559,28560,28561,28562,28563,28564,28565,28566,28567,28568,28569,28570,28571,28573,28574,28575,28576,28578,28579,28580,28581,28582,28584,28585,28586,28587,28588,28589,28590,28591,28592,28593,28594,28596,28597,28599,28600,28602,28603,28604,28605,28606,28607,28609,28611,28612,28613,28614,28615,28616,28618,28619,28620,28621,28622,28623,28624,28627,28628,28629,28630,28631,28632,28633,28634,28635,28636,28637,28639,28642,28643,28644,28645,28646,28647,28648,28649,28650,28651,28652,28653,28656,28657,28658,28659,28660,28661,28662,28663,28664,28665,28666,28667,28668,28669,28670,28671,28672,28673,28674,28675,28676,28677,28678,28679,28680,28681,28682,28683,28684,28685,28686,28687,28688,28690,28691,28692,28693,28694,28695,28696,28697,28700,28701,28702,28703,28704,28705,28706,28708,28709,28710,28711,28712,28713,28714,28715,28716,28717,28718,28719,28720,28721,28722,28723,28724,28726,28727,28728,28730,28731,28732,28733,28734,28735,28736,28737,28738,28739,28740,28741,28742,28743,28744,28745,28746,28747,28749,28750,28752,28753,28754,28755,28756,28757,28758,28759,28760,28761,28762,28763,28764,28765,28767,28768,28769,28770,28771,28772,28773,28774,28775,28776,28777,28778,28782,28785,28786,28787,28788,28791,28793,28794,28795,28797,28801,28802,28803,28804,28806,28807,28808,28811,28812,28813,28815,28816,28817,28819,28823,28824,28826,28827,28830,28831,28832,28833,28834,28835,28836,28837,28838,28839,28840,28841,28842,28848,28850,28852,28853,28854,28858,28862,28863,28868,28869,28870,28871,28873,28875,28876,28877,28878,28879,28880,28881,28882,28883,28884,28885,28886,28887,28890,28892,28893,28894,28896,28897,28898,28899,28901,28906,28910,28912,28913,28914,28915,28916,28917,28918,28920,28922,28923,28924,28926,28927,28928,28929,28930,28931,28932,28933,28934,28935,28936,28939,28940,28941,28942,28943,28945,28946,28948,28951,28955,28956,28957,28958,28959,28960,28961,28962,28963,28964,28965,28967,28968,28969,28970,28971,28972,28973,28974,28978,28979,28980,28981,28983,28984,28985,28986,28987,28988,28989,28990,28991,28992,28993,28994,28995,28996,28998,28999,29000,29001,29003,29005,29007,29008,29009,29010,29011,29012,29013,29014,29015,29016,29017,29018,29019,29021,29023,29024,29025,29026,29027,29029,29033,29034,29035,29036,29037,29039,29040,29041,29044,29045,29046,29047,29049,29051,29052,29054,29055,29056,29057,29058,29059,29061,29062,29063,29064,29065,29067,29068,29069,29070,29072,29073,29074,29075,29077,29078,29079,29082,29083,29084,29085,29086,29089,29090,29091,29092,29093,29094,29095,29097,29098,29099,29101,29102,29103,29104,29105,29106,29108,29110,29111,29112,29114,29115,29116,29117,29118,29119,29120,29121,29122,29124,29125,29126,29127,29128,29129,29130,29131,29132,29133,29135,29136,29137,29138,29139,29142,29143,29144,29145,29146,29147,29148,29149,29150,29151,29153,29154,29155,29156,29158,29160,29161,29162,29163,29164,29165,29167,29168,29169,29170,29171,29172,29173,29174,29175,29176,29178,29179,29180,29181,29182,29183,29184,29185,29186,29187,29188,29189,29191,29192,29193,29194,29195,29196,29197,29198,29199,29200,29201,29202,29203,29204,29205,29206,29207,29208,29209,29210,29211,29212,29214,29215,29216,29217,29218,29219,29220,29221,29222,29223,29225,29227,29229,29230,29231,29234,29235,29236,29242,29244,29246,29248,29249,29250,29251,29252,29253,29254,29257,29258,29259,29262,29263,29264,29265,29267,29268,29269,29271,29272,29274,29276,29278,29280,29283,29284,29285,29288,29290,29291,29292,29293,29296,29297,29299,29300,29302,29303,29304,29307,29308,29309,29314,29315,29317,29318,29319,29320,29321,29324,29326,29328,29329,29331,29332,29333,29334,29335,29336,29337,29338,29339,29340,29341,29342,29344,29345,29346,29347,29348,29349,29350,29351,29352,29353,29354,29355,29358,29361,29362,29363,29365,29370,29371,29372,29373,29374,29375,29376,29381,29382,29383,29385,29386,29387,29388,29391,29393,29395,29396,29397,29398,29400,29402,29403,58566,58567,58568,58569,58570,58571,58572,58573,58574,58575,58576,58577,58578,58579,58580,58581,58582,58583,58584,58585,58586,58587,58588,58589,58590,58591,58592,58593,58594,58595,58596,58597,58598,58599,58600,58601,58602,58603,58604,58605,58606,58607,58608,58609,58610,58611,58612,58613,58614,58615,58616,58617,58618,58619,58620,58621,58622,58623,58624,58625,58626,58627,58628,58629,58630,58631,58632,58633,58634,58635,58636,58637,58638,58639,58640,58641,58642,58643,58644,58645,58646,58647,58648,58649,58650,58651,58652,58653,58654,58655,58656,58657,58658,58659,58660,58661,12288,12289,12290,183,713,711,168,12291,12293,8212,65374,8214,8230,8216,8217,8220,8221,12308,12309,12296,12297,12298,12299,12300,12301,12302,12303,12310,12311,12304,12305,177,215,247,8758,8743,8744,8721,8719,8746,8745,8712,8759,8730,8869,8741,8736,8978,8857,8747,8750,8801,8780,8776,8765,8733,8800,8814,8815,8804,8805,8734,8757,8756,9794,9792,176,8242,8243,8451,65284,164,65504,65505,8240,167,8470,9734,9733,9675,9679,9678,9671,9670,9633,9632,9651,9650,8251,8594,8592,8593,8595,12307,58662,58663,58664,58665,58666,58667,58668,58669,58670,58671,58672,58673,58674,58675,58676,58677,58678,58679,58680,58681,58682,58683,58684,58685,58686,58687,58688,58689,58690,58691,58692,58693,58694,58695,58696,58697,58698,58699,58700,58701,58702,58703,58704,58705,58706,58707,58708,58709,58710,58711,58712,58713,58714,58715,58716,58717,58718,58719,58720,58721,58722,58723,58724,58725,58726,58727,58728,58729,58730,58731,58732,58733,58734,58735,58736,58737,58738,58739,58740,58741,58742,58743,58744,58745,58746,58747,58748,58749,58750,58751,58752,58753,58754,58755,58756,58757,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,59238,59239,59240,59241,59242,59243,9352,9353,9354,9355,9356,9357,9358,9359,9360,9361,9362,9363,9364,9365,9366,9367,9368,9369,9370,9371,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,9342,9343,9344,9345,9346,9347,9348,9349,9350,9351,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,8364,59245,12832,12833,12834,12835,12836,12837,12838,12839,12840,12841,59246,59247,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554,8555,59248,59249,58758,58759,58760,58761,58762,58763,58764,58765,58766,58767,58768,58769,58770,58771,58772,58773,58774,58775,58776,58777,58778,58779,58780,58781,58782,58783,58784,58785,58786,58787,58788,58789,58790,58791,58792,58793,58794,58795,58796,58797,58798,58799,58800,58801,58802,58803,58804,58805,58806,58807,58808,58809,58810,58811,58812,58813,58814,58815,58816,58817,58818,58819,58820,58821,58822,58823,58824,58825,58826,58827,58828,58829,58830,58831,58832,58833,58834,58835,58836,58837,58838,58839,58840,58841,58842,58843,58844,58845,58846,58847,58848,58849,58850,58851,58852,12288,65281,65282,65283,65509,65285,65286,65287,65288,65289,65290,65291,65292,65293,65294,65295,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,65306,65307,65308,65309,65310,65311,65312,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65339,65340,65341,65342,65343,65344,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,65371,65372,65373,65507,58854,58855,58856,58857,58858,58859,58860,58861,58862,58863,58864,58865,58866,58867,58868,58869,58870,58871,58872,58873,58874,58875,58876,58877,58878,58879,58880,58881,58882,58883,58884,58885,58886,58887,58888,58889,58890,58891,58892,58893,58894,58895,58896,58897,58898,58899,58900,58901,58902,58903,58904,58905,58906,58907,58908,58909,58910,58911,58912,58913,58914,58915,58916,58917,58918,58919,58920,58921,58922,58923,58924,58925,58926,58927,58928,58929,58930,58931,58932,58933,58934,58935,58936,58937,58938,58939,58940,58941,58942,58943,58944,58945,58946,58947,58948,58949,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,59250,59251,59252,59253,59254,59255,59256,59257,59258,59259,59260,58950,58951,58952,58953,58954,58955,58956,58957,58958,58959,58960,58961,58962,58963,58964,58965,58966,58967,58968,58969,58970,58971,58972,58973,58974,58975,58976,58977,58978,58979,58980,58981,58982,58983,58984,58985,58986,58987,58988,58989,58990,58991,58992,58993,58994,58995,58996,58997,58998,58999,59000,59001,59002,59003,59004,59005,59006,59007,59008,59009,59010,59011,59012,59013,59014,59015,59016,59017,59018,59019,59020,59021,59022,59023,59024,59025,59026,59027,59028,59029,59030,59031,59032,59033,59034,59035,59036,59037,59038,59039,59040,59041,59042,59043,59044,59045,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,59261,59262,59263,59264,59265,59266,59267,59268,59046,59047,59048,59049,59050,59051,59052,59053,59054,59055,59056,59057,59058,59059,59060,59061,59062,59063,59064,59065,59066,59067,59068,59069,59070,59071,59072,59073,59074,59075,59076,59077,59078,59079,59080,59081,59082,59083,59084,59085,59086,59087,59088,59089,59090,59091,59092,59093,59094,59095,59096,59097,59098,59099,59100,59101,59102,59103,59104,59105,59106,59107,59108,59109,59110,59111,59112,59113,59114,59115,59116,59117,59118,59119,59120,59121,59122,59123,59124,59125,59126,59127,59128,59129,59130,59131,59132,59133,59134,59135,59136,59137,59138,59139,59140,59141,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,59269,59270,59271,59272,59273,59274,59275,59276,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,59277,59278,59279,59280,59281,59282,59283,65077,65078,65081,65082,65087,65088,65085,65086,65089,65090,65091,65092,59284,59285,65083,65084,65079,65080,65073,59286,65075,65076,59287,59288,59289,59290,59291,59292,59293,59294,59295,59142,59143,59144,59145,59146,59147,59148,59149,59150,59151,59152,59153,59154,59155,59156,59157,59158,59159,59160,59161,59162,59163,59164,59165,59166,59167,59168,59169,59170,59171,59172,59173,59174,59175,59176,59177,59178,59179,59180,59181,59182,59183,59184,59185,59186,59187,59188,59189,59190,59191,59192,59193,59194,59195,59196,59197,59198,59199,59200,59201,59202,59203,59204,59205,59206,59207,59208,59209,59210,59211,59212,59213,59214,59215,59216,59217,59218,59219,59220,59221,59222,59223,59224,59225,59226,59227,59228,59229,59230,59231,59232,59233,59234,59235,59236,59237,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,59296,59297,59298,59299,59300,59301,59302,59303,59304,59305,59306,59307,59308,59309,59310,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,59311,59312,59313,59314,59315,59316,59317,59318,59319,59320,59321,59322,59323,714,715,729,8211,8213,8229,8245,8453,8457,8598,8599,8600,8601,8725,8735,8739,8786,8806,8807,8895,9552,9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9581,9582,9583,9584,9585,9586,9587,9601,9602,9603,9604,9605,9606,9607,9608,9609,9610,9611,9612,9613,9614,9615,9619,9620,9621,9660,9661,9698,9699,9700,9701,9737,8853,12306,12317,12318,59324,59325,59326,59327,59328,59329,59330,59331,59332,59333,59334,257,225,462,224,275,233,283,232,299,237,464,236,333,243,466,242,363,250,468,249,470,472,474,476,252,234,593,7743,324,328,505,609,59337,59338,59339,59340,12549,12550,12551,12552,12553,12554,12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570,12571,12572,12573,12574,12575,12576,12577,12578,12579,12580,12581,12582,12583,12584,12585,59341,59342,59343,59344,59345,59346,59347,59348,59349,59350,59351,59352,59353,59354,59355,59356,59357,59358,59359,59360,59361,12321,12322,12323,12324,12325,12326,12327,12328,12329,12963,13198,13199,13212,13213,13214,13217,13252,13262,13265,13266,13269,65072,65506,65508,59362,8481,12849,59363,8208,59364,59365,59366,12540,12443,12444,12541,12542,12294,12445,12446,65097,65098,65099,65100,65101,65102,65103,65104,65105,65106,65108,65109,65110,65111,65113,65114,65115,65116,65117,65118,65119,65120,65121,65122,65123,65124,65125,65126,65128,65129,65130,65131,12350,12272,12273,12274,12275,12276,12277,12278,12279,12280,12281,12282,12283,12295,59380,59381,59382,59383,59384,59385,59386,59387,59388,59389,59390,59391,59392,9472,9473,9474,9475,9476,9477,9478,9479,9480,9481,9482,9483,9484,9485,9486,9487,9488,9489,9490,9491,9492,9493,9494,9495,9496,9497,9498,9499,9500,9501,9502,9503,9504,9505,9506,9507,9508,9509,9510,9511,9512,9513,9514,9515,9516,9517,9518,9519,9520,9521,9522,9523,9524,9525,9526,9527,9528,9529,9530,9531,9532,9533,9534,9535,9536,9537,9538,9539,9540,9541,9542,9543,9544,9545,9546,9547,59393,59394,59395,59396,59397,59398,59399,59400,59401,59402,59403,59404,59405,59406,59407,29404,29405,29407,29410,29411,29412,29413,29414,29415,29418,29419,29429,29430,29433,29437,29438,29439,29440,29442,29444,29445,29446,29447,29448,29449,29451,29452,29453,29455,29456,29457,29458,29460,29464,29465,29466,29471,29472,29475,29476,29478,29479,29480,29485,29487,29488,29490,29491,29493,29494,29498,29499,29500,29501,29504,29505,29506,29507,29508,29509,29510,29511,29512,29513,29514,29515,29516,29518,29519,29521,29523,29524,29525,29526,29528,29529,29530,29531,29532,29533,29534,29535,29537,29538,29539,29540,29541,29542,29543,29544,29545,29546,29547,29550,29552,29553,57344,57345,57346,57347,57348,57349,57350,57351,57352,57353,57354,57355,57356,57357,57358,57359,57360,57361,57362,57363,57364,57365,57366,57367,57368,57369,57370,57371,57372,57373,57374,57375,57376,57377,57378,57379,57380,57381,57382,57383,57384,57385,57386,57387,57388,57389,57390,57391,57392,57393,57394,57395,57396,57397,57398,57399,57400,57401,57402,57403,57404,57405,57406,57407,57408,57409,57410,57411,57412,57413,57414,57415,57416,57417,57418,57419,57420,57421,57422,57423,57424,57425,57426,57427,57428,57429,57430,57431,57432,57433,57434,57435,57436,57437,29554,29555,29556,29557,29558,29559,29560,29561,29562,29563,29564,29565,29567,29568,29569,29570,29571,29573,29574,29576,29578,29580,29581,29583,29584,29586,29587,29588,29589,29591,29592,29593,29594,29596,29597,29598,29600,29601,29603,29604,29605,29606,29607,29608,29610,29612,29613,29617,29620,29621,29622,29624,29625,29628,29629,29630,29631,29633,29635,29636,29637,29638,29639,29643,29644,29646,29650,29651,29652,29653,29654,29655,29656,29658,29659,29660,29661,29663,29665,29666,29667,29668,29670,29672,29674,29675,29676,29678,29679,29680,29681,29683,29684,29685,29686,29687,57438,57439,57440,57441,57442,57443,57444,57445,57446,57447,57448,57449,57450,57451,57452,57453,57454,57455,57456,57457,57458,57459,57460,57461,57462,57463,57464,57465,57466,57467,57468,57469,57470,57471,57472,57473,57474,57475,57476,57477,57478,57479,57480,57481,57482,57483,57484,57485,57486,57487,57488,57489,57490,57491,57492,57493,57494,57495,57496,57497,57498,57499,57500,57501,57502,57503,57504,57505,57506,57507,57508,57509,57510,57511,57512,57513,57514,57515,57516,57517,57518,57519,57520,57521,57522,57523,57524,57525,57526,57527,57528,57529,57530,57531,29688,29689,29690,29691,29692,29693,29694,29695,29696,29697,29698,29700,29703,29704,29707,29708,29709,29710,29713,29714,29715,29716,29717,29718,29719,29720,29721,29724,29725,29726,29727,29728,29729,29731,29732,29735,29737,29739,29741,29743,29745,29746,29751,29752,29753,29754,29755,29757,29758,29759,29760,29762,29763,29764,29765,29766,29767,29768,29769,29770,29771,29772,29773,29774,29775,29776,29777,29778,29779,29780,29782,29784,29789,29792,29793,29794,29795,29796,29797,29798,29799,29800,29801,29802,29803,29804,29806,29807,29809,29810,29811,29812,29813,29816,29817,29818,57532,57533,57534,57535,57536,57537,57538,57539,57540,57541,57542,57543,57544,57545,57546,57547,57548,57549,57550,57551,57552,57553,57554,57555,57556,57557,57558,57559,57560,57561,57562,57563,57564,57565,57566,57567,57568,57569,57570,57571,57572,57573,57574,57575,57576,57577,57578,57579,57580,57581,57582,57583,57584,57585,57586,57587,57588,57589,57590,57591,57592,57593,57594,57595,57596,57597,57598,57599,57600,57601,57602,57603,57604,57605,57606,57607,57608,57609,57610,57611,57612,57613,57614,57615,57616,57617,57618,57619,57620,57621,57622,57623,57624,57625,29819,29820,29821,29823,29826,29828,29829,29830,29832,29833,29834,29836,29837,29839,29841,29842,29843,29844,29845,29846,29847,29848,29849,29850,29851,29853,29855,29856,29857,29858,29859,29860,29861,29862,29866,29867,29868,29869,29870,29871,29872,29873,29874,29875,29876,29877,29878,29879,29880,29881,29883,29884,29885,29886,29887,29888,29889,29890,29891,29892,29893,29894,29895,29896,29897,29898,29899,29900,29901,29902,29903,29904,29905,29907,29908,29909,29910,29911,29912,29913,29914,29915,29917,29919,29921,29925,29927,29928,29929,29930,29931,29932,29933,29936,29937,29938,57626,57627,57628,57629,57630,57631,57632,57633,57634,57635,57636,57637,57638,57639,57640,57641,57642,57643,57644,57645,57646,57647,57648,57649,57650,57651,57652,57653,57654,57655,57656,57657,57658,57659,57660,57661,57662,57663,57664,57665,57666,57667,57668,57669,57670,57671,57672,57673,57674,57675,57676,57677,57678,57679,57680,57681,57682,57683,57684,57685,57686,57687,57688,57689,57690,57691,57692,57693,57694,57695,57696,57697,57698,57699,57700,57701,57702,57703,57704,57705,57706,57707,57708,57709,57710,57711,57712,57713,57714,57715,57716,57717,57718,57719,29939,29941,29944,29945,29946,29947,29948,29949,29950,29952,29953,29954,29955,29957,29958,29959,29960,29961,29962,29963,29964,29966,29968,29970,29972,29973,29974,29975,29979,29981,29982,29984,29985,29986,29987,29988,29990,29991,29994,29998,30004,30006,30009,30012,30013,30015,30017,30018,30019,30020,30022,30023,30025,30026,30029,30032,30033,30034,30035,30037,30038,30039,30040,30045,30046,30047,30048,30049,30050,30051,30052,30055,30056,30057,30059,30060,30061,30062,30063,30064,30065,30067,30069,30070,30071,30074,30075,30076,30077,30078,30080,30081,30082,30084,30085,30087,57720,57721,57722,57723,57724,57725,57726,57727,57728,57729,57730,57731,57732,57733,57734,57735,57736,57737,57738,57739,57740,57741,57742,57743,57744,57745,57746,57747,57748,57749,57750,57751,57752,57753,57754,57755,57756,57757,57758,57759,57760,57761,57762,57763,57764,57765,57766,57767,57768,57769,57770,57771,57772,57773,57774,57775,57776,57777,57778,57779,57780,57781,57782,57783,57784,57785,57786,57787,57788,57789,57790,57791,57792,57793,57794,57795,57796,57797,57798,57799,57800,57801,57802,57803,57804,57805,57806,57807,57808,57809,57810,57811,57812,57813,30088,30089,30090,30092,30093,30094,30096,30099,30101,30104,30107,30108,30110,30114,30118,30119,30120,30121,30122,30125,30134,30135,30138,30139,30143,30144,30145,30150,30155,30156,30158,30159,30160,30161,30163,30167,30169,30170,30172,30173,30175,30176,30177,30181,30185,30188,30189,30190,30191,30194,30195,30197,30198,30199,30200,30202,30203,30205,30206,30210,30212,30214,30215,30216,30217,30219,30221,30222,30223,30225,30226,30227,30228,30230,30234,30236,30237,30238,30241,30243,30247,30248,30252,30254,30255,30257,30258,30262,30263,30265,30266,30267,30269,30273,30274,30276,57814,57815,57816,57817,57818,57819,57820,57821,57822,57823,57824,57825,57826,57827,57828,57829,57830,57831,57832,57833,57834,57835,57836,57837,57838,57839,57840,57841,57842,57843,57844,57845,57846,57847,57848,57849,57850,57851,57852,57853,57854,57855,57856,57857,57858,57859,57860,57861,57862,57863,57864,57865,57866,57867,57868,57869,57870,57871,57872,57873,57874,57875,57876,57877,57878,57879,57880,57881,57882,57883,57884,57885,57886,57887,57888,57889,57890,57891,57892,57893,57894,57895,57896,57897,57898,57899,57900,57901,57902,57903,57904,57905,57906,57907,30277,30278,30279,30280,30281,30282,30283,30286,30287,30288,30289,30290,30291,30293,30295,30296,30297,30298,30299,30301,30303,30304,30305,30306,30308,30309,30310,30311,30312,30313,30314,30316,30317,30318,30320,30321,30322,30323,30324,30325,30326,30327,30329,30330,30332,30335,30336,30337,30339,30341,30345,30346,30348,30349,30351,30352,30354,30356,30357,30359,30360,30362,30363,30364,30365,30366,30367,30368,30369,30370,30371,30373,30374,30375,30376,30377,30378,30379,30380,30381,30383,30384,30387,30389,30390,30391,30392,30393,30394,30395,30396,30397,30398,30400,30401,30403,21834,38463,22467,25384,21710,21769,21696,30353,30284,34108,30702,33406,30861,29233,38552,38797,27688,23433,20474,25353,26263,23736,33018,26696,32942,26114,30414,20985,25942,29100,32753,34948,20658,22885,25034,28595,33453,25420,25170,21485,21543,31494,20843,30116,24052,25300,36299,38774,25226,32793,22365,38712,32610,29240,30333,26575,30334,25670,20336,36133,25308,31255,26001,29677,25644,25203,33324,39041,26495,29256,25198,25292,20276,29923,21322,21150,32458,37030,24110,26758,27036,33152,32465,26834,30917,34444,38225,20621,35876,33502,32990,21253,35090,21093,30404,30407,30409,30411,30412,30419,30421,30425,30426,30428,30429,30430,30432,30433,30434,30435,30436,30438,30439,30440,30441,30442,30443,30444,30445,30448,30451,30453,30454,30455,30458,30459,30461,30463,30464,30466,30467,30469,30470,30474,30476,30478,30479,30480,30481,30482,30483,30484,30485,30486,30487,30488,30491,30492,30493,30494,30497,30499,30500,30501,30503,30506,30507,30508,30510,30512,30513,30514,30515,30516,30521,30523,30525,30526,30527,30530,30532,30533,30534,30536,30537,30538,30539,30540,30541,30542,30543,30546,30547,30548,30549,30550,30551,30552,30553,30556,34180,38649,20445,22561,39281,23453,25265,25253,26292,35961,40077,29190,26479,30865,24754,21329,21271,36744,32972,36125,38049,20493,29384,22791,24811,28953,34987,22868,33519,26412,31528,23849,32503,29997,27893,36454,36856,36924,40763,27604,37145,31508,24444,30887,34006,34109,27605,27609,27606,24065,24199,30201,38381,25949,24330,24517,36767,22721,33218,36991,38491,38829,36793,32534,36140,25153,20415,21464,21342,36776,36777,36779,36941,26631,24426,33176,34920,40150,24971,21035,30250,24428,25996,28626,28392,23486,25672,20853,20912,26564,19993,31177,39292,28851,30557,30558,30559,30560,30564,30567,30569,30570,30573,30574,30575,30576,30577,30578,30579,30580,30581,30582,30583,30584,30586,30587,30588,30593,30594,30595,30598,30599,30600,30601,30602,30603,30607,30608,30611,30612,30613,30614,30615,30616,30617,30618,30619,30620,30621,30622,30625,30627,30628,30630,30632,30635,30637,30638,30639,30641,30642,30644,30646,30647,30648,30649,30650,30652,30654,30656,30657,30658,30659,30660,30661,30662,30663,30664,30665,30666,30667,30668,30670,30671,30672,30673,30674,30675,30676,30677,30678,30680,30681,30682,30685,30686,30687,30688,30689,30692,30149,24182,29627,33760,25773,25320,38069,27874,21338,21187,25615,38082,31636,20271,24091,33334,33046,33162,28196,27850,39539,25429,21340,21754,34917,22496,19981,24067,27493,31807,37096,24598,25830,29468,35009,26448,25165,36130,30572,36393,37319,24425,33756,34081,39184,21442,34453,27531,24813,24808,28799,33485,33329,20179,27815,34255,25805,31961,27133,26361,33609,21397,31574,20391,20876,27979,23618,36461,25554,21449,33580,33590,26597,30900,25661,23519,23700,24046,35815,25286,26612,35962,25600,25530,34633,39307,35863,32544,38130,20135,38416,39076,26124,29462,30694,30696,30698,30703,30704,30705,30706,30708,30709,30711,30713,30714,30715,30716,30723,30724,30725,30726,30727,30728,30730,30731,30734,30735,30736,30739,30741,30745,30747,30750,30752,30753,30754,30756,30760,30762,30763,30766,30767,30769,30770,30771,30773,30774,30781,30783,30785,30786,30787,30788,30790,30792,30793,30794,30795,30797,30799,30801,30803,30804,30808,30809,30810,30811,30812,30814,30815,30816,30817,30818,30819,30820,30821,30822,30823,30824,30825,30831,30832,30833,30834,30835,30836,30837,30838,30840,30841,30842,30843,30845,30846,30847,30848,30849,30850,30851,22330,23581,24120,38271,20607,32928,21378,25950,30021,21809,20513,36229,25220,38046,26397,22066,28526,24034,21557,28818,36710,25199,25764,25507,24443,28552,37108,33251,36784,23576,26216,24561,27785,38472,36225,34924,25745,31216,22478,27225,25104,21576,20056,31243,24809,28548,35802,25215,36894,39563,31204,21507,30196,25345,21273,27744,36831,24347,39536,32827,40831,20360,23610,36196,32709,26021,28861,20805,20914,34411,23815,23456,25277,37228,30068,36364,31264,24833,31609,20167,32504,30597,19985,33261,21021,20986,27249,21416,36487,38148,38607,28353,38500,26970,30852,30853,30854,30856,30858,30859,30863,30864,30866,30868,30869,30870,30873,30877,30878,30880,30882,30884,30886,30888,30889,30890,30891,30892,30893,30894,30895,30901,30902,30903,30904,30906,30907,30908,30909,30911,30912,30914,30915,30916,30918,30919,30920,30924,30925,30926,30927,30929,30930,30931,30934,30935,30936,30938,30939,30940,30941,30942,30943,30944,30945,30946,30947,30948,30949,30950,30951,30953,30954,30955,30957,30958,30959,30960,30961,30963,30965,30966,30968,30969,30971,30972,30973,30974,30975,30976,30978,30979,30980,30982,30983,30984,30985,30986,30987,30988,30784,20648,30679,25616,35302,22788,25571,24029,31359,26941,20256,33337,21912,20018,30126,31383,24162,24202,38383,21019,21561,28810,25462,38180,22402,26149,26943,37255,21767,28147,32431,34850,25139,32496,30133,33576,30913,38604,36766,24904,29943,35789,27492,21050,36176,27425,32874,33905,22257,21254,20174,19995,20945,31895,37259,31751,20419,36479,31713,31388,25703,23828,20652,33030,30209,31929,28140,32736,26449,23384,23544,30923,25774,25619,25514,25387,38169,25645,36798,31572,30249,25171,22823,21574,27513,20643,25140,24102,27526,20195,36151,34955,24453,36910,30989,30990,30991,30992,30993,30994,30996,30997,30998,30999,31000,31001,31002,31003,31004,31005,31007,31008,31009,31010,31011,31013,31014,31015,31016,31017,31018,31019,31020,31021,31022,31023,31024,31025,31026,31027,31029,31030,31031,31032,31033,31037,31039,31042,31043,31044,31045,31047,31050,31051,31052,31053,31054,31055,31056,31057,31058,31060,31061,31064,31065,31073,31075,31076,31078,31081,31082,31083,31084,31086,31088,31089,31090,31091,31092,31093,31094,31097,31099,31100,31101,31102,31103,31106,31107,31110,31111,31112,31113,31115,31116,31117,31118,31120,31121,31122,24608,32829,25285,20025,21333,37112,25528,32966,26086,27694,20294,24814,28129,35806,24377,34507,24403,25377,20826,33633,26723,20992,25443,36424,20498,23707,31095,23548,21040,31291,24764,36947,30423,24503,24471,30340,36460,28783,30331,31561,30634,20979,37011,22564,20302,28404,36842,25932,31515,29380,28068,32735,23265,25269,24213,22320,33922,31532,24093,24351,36882,32532,39072,25474,28359,30872,28857,20856,38747,22443,30005,20291,30008,24215,24806,22880,28096,27583,30857,21500,38613,20939,20993,25481,21514,38035,35843,36300,29241,30879,34678,36845,35853,21472,31123,31124,31125,31126,31127,31128,31129,31131,31132,31133,31134,31135,31136,31137,31138,31139,31140,31141,31142,31144,31145,31146,31147,31148,31149,31150,31151,31152,31153,31154,31156,31157,31158,31159,31160,31164,31167,31170,31172,31173,31175,31176,31178,31180,31182,31183,31184,31187,31188,31190,31191,31193,31194,31195,31196,31197,31198,31200,31201,31202,31205,31208,31210,31212,31214,31217,31218,31219,31220,31221,31222,31223,31225,31226,31228,31230,31231,31233,31236,31237,31239,31240,31241,31242,31244,31247,31248,31249,31250,31251,31253,31254,31256,31257,31259,31260,19969,30447,21486,38025,39030,40718,38189,23450,35746,20002,19996,20908,33891,25026,21160,26635,20375,24683,20923,27934,20828,25238,26007,38497,35910,36887,30168,37117,30563,27602,29322,29420,35835,22581,30585,36172,26460,38208,32922,24230,28193,22930,31471,30701,38203,27573,26029,32526,22534,20817,38431,23545,22697,21544,36466,25958,39039,22244,38045,30462,36929,25479,21702,22810,22842,22427,36530,26421,36346,33333,21057,24816,22549,34558,23784,40517,20420,39069,35769,23077,24694,21380,25212,36943,37122,39295,24681,32780,20799,32819,23572,39285,27953,20108,31261,31263,31265,31266,31268,31269,31270,31271,31272,31273,31274,31275,31276,31277,31278,31279,31280,31281,31282,31284,31285,31286,31288,31290,31294,31296,31297,31298,31299,31300,31301,31303,31304,31305,31306,31307,31308,31309,31310,31311,31312,31314,31315,31316,31317,31318,31320,31321,31322,31323,31324,31325,31326,31327,31328,31329,31330,31331,31332,31333,31334,31335,31336,31337,31338,31339,31340,31341,31342,31343,31345,31346,31347,31349,31355,31356,31357,31358,31362,31365,31367,31369,31370,31371,31372,31374,31375,31376,31379,31380,31385,31386,31387,31390,31393,31394,36144,21457,32602,31567,20240,20047,38400,27861,29648,34281,24070,30058,32763,27146,30718,38034,32321,20961,28902,21453,36820,33539,36137,29359,39277,27867,22346,33459,26041,32938,25151,38450,22952,20223,35775,32442,25918,33778,38750,21857,39134,32933,21290,35837,21536,32954,24223,27832,36153,33452,37210,21545,27675,20998,32439,22367,28954,27774,31881,22859,20221,24575,24868,31914,20016,23553,26539,34562,23792,38155,39118,30127,28925,36898,20911,32541,35773,22857,20964,20315,21542,22827,25975,32932,23413,25206,25282,36752,24133,27679,31526,20239,20440,26381,31395,31396,31399,31401,31402,31403,31406,31407,31408,31409,31410,31412,31413,31414,31415,31416,31417,31418,31419,31420,31421,31422,31424,31425,31426,31427,31428,31429,31430,31431,31432,31433,31434,31436,31437,31438,31439,31440,31441,31442,31443,31444,31445,31447,31448,31450,31451,31452,31453,31457,31458,31460,31463,31464,31465,31466,31467,31468,31470,31472,31473,31474,31475,31476,31477,31478,31479,31480,31483,31484,31486,31488,31489,31490,31493,31495,31497,31500,31501,31502,31504,31506,31507,31510,31511,31512,31514,31516,31517,31519,31521,31522,31523,31527,31529,31533,28014,28074,31119,34993,24343,29995,25242,36741,20463,37340,26023,33071,33105,24220,33104,36212,21103,35206,36171,22797,20613,20184,38428,29238,33145,36127,23500,35747,38468,22919,32538,21648,22134,22030,35813,25913,27010,38041,30422,28297,24178,29976,26438,26577,31487,32925,36214,24863,31174,25954,36195,20872,21018,38050,32568,32923,32434,23703,28207,26464,31705,30347,39640,33167,32660,31957,25630,38224,31295,21578,21733,27468,25601,25096,40509,33011,30105,21106,38761,33883,26684,34532,38401,38548,38124,20010,21508,32473,26681,36319,32789,26356,24218,32697,31535,31536,31538,31540,31541,31542,31543,31545,31547,31549,31551,31552,31553,31554,31555,31556,31558,31560,31562,31565,31566,31571,31573,31575,31577,31580,31582,31583,31585,31587,31588,31589,31590,31591,31592,31593,31594,31595,31596,31597,31599,31600,31603,31604,31606,31608,31610,31612,31613,31615,31617,31618,31619,31620,31622,31623,31624,31625,31626,31627,31628,31630,31631,31633,31634,31635,31638,31640,31641,31642,31643,31646,31647,31648,31651,31652,31653,31662,31663,31664,31666,31667,31669,31670,31671,31673,31674,31675,31676,31677,31678,31679,31680,31682,31683,31684,22466,32831,26775,24037,25915,21151,24685,40858,20379,36524,20844,23467,24339,24041,27742,25329,36129,20849,38057,21246,27807,33503,29399,22434,26500,36141,22815,36764,33735,21653,31629,20272,27837,23396,22993,40723,21476,34506,39592,35895,32929,25925,39038,22266,38599,21038,29916,21072,23521,25346,35074,20054,25296,24618,26874,20851,23448,20896,35266,31649,39302,32592,24815,28748,36143,20809,24191,36891,29808,35268,22317,30789,24402,40863,38394,36712,39740,35809,30328,26690,26588,36330,36149,21053,36746,28378,26829,38149,37101,22269,26524,35065,36807,21704,31685,31688,31689,31690,31691,31693,31694,31695,31696,31698,31700,31701,31702,31703,31704,31707,31708,31710,31711,31712,31714,31715,31716,31719,31720,31721,31723,31724,31725,31727,31728,31730,31731,31732,31733,31734,31736,31737,31738,31739,31741,31743,31744,31745,31746,31747,31748,31749,31750,31752,31753,31754,31757,31758,31760,31761,31762,31763,31764,31765,31767,31768,31769,31770,31771,31772,31773,31774,31776,31777,31778,31779,31780,31781,31784,31785,31787,31788,31789,31790,31791,31792,31793,31794,31795,31796,31797,31798,31799,31801,31802,31803,31804,31805,31806,31810,39608,23401,28023,27686,20133,23475,39559,37219,25000,37039,38889,21547,28085,23506,20989,21898,32597,32752,25788,25421,26097,25022,24717,28938,27735,27721,22831,26477,33322,22741,22158,35946,27627,37085,22909,32791,21495,28009,21621,21917,33655,33743,26680,31166,21644,20309,21512,30418,35977,38402,27827,28088,36203,35088,40548,36154,22079,40657,30165,24456,29408,24680,21756,20136,27178,34913,24658,36720,21700,28888,34425,40511,27946,23439,24344,32418,21897,20399,29492,21564,21402,20505,21518,21628,20046,24573,29786,22774,33899,32993,34676,29392,31946,28246,31811,31812,31813,31814,31815,31816,31817,31818,31819,31820,31822,31823,31824,31825,31826,31827,31828,31829,31830,31831,31832,31833,31834,31835,31836,31837,31838,31839,31840,31841,31842,31843,31844,31845,31846,31847,31848,31849,31850,31851,31852,31853,31854,31855,31856,31857,31858,31861,31862,31863,31864,31865,31866,31870,31871,31872,31873,31874,31875,31876,31877,31878,31879,31880,31882,31883,31884,31885,31886,31887,31888,31891,31892,31894,31897,31898,31899,31904,31905,31907,31910,31911,31912,31913,31915,31916,31917,31919,31920,31924,31925,31926,31927,31928,31930,31931,24359,34382,21804,25252,20114,27818,25143,33457,21719,21326,29502,28369,30011,21010,21270,35805,27088,24458,24576,28142,22351,27426,29615,26707,36824,32531,25442,24739,21796,30186,35938,28949,28067,23462,24187,33618,24908,40644,30970,34647,31783,30343,20976,24822,29004,26179,24140,24653,35854,28784,25381,36745,24509,24674,34516,22238,27585,24724,24935,21321,24800,26214,36159,31229,20250,28905,27719,35763,35826,32472,33636,26127,23130,39746,27985,28151,35905,27963,20249,28779,33719,25110,24785,38669,36135,31096,20987,22334,22522,26426,30072,31293,31215,31637,31935,31936,31938,31939,31940,31942,31945,31947,31950,31951,31952,31953,31954,31955,31956,31960,31962,31963,31965,31966,31969,31970,31971,31972,31973,31974,31975,31977,31978,31979,31980,31981,31982,31984,31985,31986,31987,31988,31989,31990,31991,31993,31994,31996,31997,31998,31999,32000,32001,32002,32003,32004,32005,32006,32007,32008,32009,32011,32012,32013,32014,32015,32016,32017,32018,32019,32020,32021,32022,32023,32024,32025,32026,32027,32028,32029,32030,32031,32033,32035,32036,32037,32038,32040,32041,32042,32044,32045,32046,32048,32049,32050,32051,32052,32053,32054,32908,39269,36857,28608,35749,40481,23020,32489,32521,21513,26497,26840,36753,31821,38598,21450,24613,30142,27762,21363,23241,32423,25380,20960,33034,24049,34015,25216,20864,23395,20238,31085,21058,24760,27982,23492,23490,35745,35760,26082,24524,38469,22931,32487,32426,22025,26551,22841,20339,23478,21152,33626,39050,36158,30002,38078,20551,31292,20215,26550,39550,23233,27516,30417,22362,23574,31546,38388,29006,20860,32937,33392,22904,32516,33575,26816,26604,30897,30839,25315,25441,31616,20461,21098,20943,33616,27099,37492,36341,36145,35265,38190,31661,20214,32055,32056,32057,32058,32059,32060,32061,32062,32063,32064,32065,32066,32067,32068,32069,32070,32071,32072,32073,32074,32075,32076,32077,32078,32079,32080,32081,32082,32083,32084,32085,32086,32087,32088,32089,32090,32091,32092,32093,32094,32095,32096,32097,32098,32099,32100,32101,32102,32103,32104,32105,32106,32107,32108,32109,32111,32112,32113,32114,32115,32116,32117,32118,32120,32121,32122,32123,32124,32125,32126,32127,32128,32129,32130,32131,32132,32133,32134,32135,32136,32137,32138,32139,32140,32141,32142,32143,32144,32145,32146,32147,32148,32149,32150,32151,32152,20581,33328,21073,39279,28176,28293,28071,24314,20725,23004,23558,27974,27743,30086,33931,26728,22870,35762,21280,37233,38477,34121,26898,30977,28966,33014,20132,37066,27975,39556,23047,22204,25605,38128,30699,20389,33050,29409,35282,39290,32564,32478,21119,25945,37237,36735,36739,21483,31382,25581,25509,30342,31224,34903,38454,25130,21163,33410,26708,26480,25463,30571,31469,27905,32467,35299,22992,25106,34249,33445,30028,20511,20171,30117,35819,23626,24062,31563,26020,37329,20170,27941,35167,32039,38182,20165,35880,36827,38771,26187,31105,36817,28908,28024,32153,32154,32155,32156,32157,32158,32159,32160,32161,32162,32163,32164,32165,32167,32168,32169,32170,32171,32172,32173,32175,32176,32177,32178,32179,32180,32181,32182,32183,32184,32185,32186,32187,32188,32189,32190,32191,32192,32193,32194,32195,32196,32197,32198,32199,32200,32201,32202,32203,32204,32205,32206,32207,32208,32209,32210,32211,32212,32213,32214,32215,32216,32217,32218,32219,32220,32221,32222,32223,32224,32225,32226,32227,32228,32229,32230,32231,32232,32233,32234,32235,32236,32237,32238,32239,32240,32241,32242,32243,32244,32245,32246,32247,32248,32249,32250,23613,21170,33606,20834,33550,30555,26230,40120,20140,24778,31934,31923,32463,20117,35686,26223,39048,38745,22659,25964,38236,24452,30153,38742,31455,31454,20928,28847,31384,25578,31350,32416,29590,38893,20037,28792,20061,37202,21417,25937,26087,33276,33285,21646,23601,30106,38816,25304,29401,30141,23621,39545,33738,23616,21632,30697,20030,27822,32858,25298,25454,24040,20855,36317,36382,38191,20465,21477,24807,28844,21095,25424,40515,23071,20518,30519,21367,32482,25733,25899,25225,25496,20500,29237,35273,20915,35776,32477,22343,33740,38055,20891,21531,23803,32251,32252,32253,32254,32255,32256,32257,32258,32259,32260,32261,32262,32263,32264,32265,32266,32267,32268,32269,32270,32271,32272,32273,32274,32275,32276,32277,32278,32279,32280,32281,32282,32283,32284,32285,32286,32287,32288,32289,32290,32291,32292,32293,32294,32295,32296,32297,32298,32299,32300,32301,32302,32303,32304,32305,32306,32307,32308,32309,32310,32311,32312,32313,32314,32316,32317,32318,32319,32320,32322,32323,32324,32325,32326,32328,32329,32330,32331,32332,32333,32334,32335,32336,32337,32338,32339,32340,32341,32342,32343,32344,32345,32346,32347,32348,32349,20426,31459,27994,37089,39567,21888,21654,21345,21679,24320,25577,26999,20975,24936,21002,22570,21208,22350,30733,30475,24247,24951,31968,25179,25239,20130,28821,32771,25335,28900,38752,22391,33499,26607,26869,30933,39063,31185,22771,21683,21487,28212,20811,21051,23458,35838,32943,21827,22438,24691,22353,21549,31354,24656,23380,25511,25248,21475,25187,23495,26543,21741,31391,33510,37239,24211,35044,22840,22446,25358,36328,33007,22359,31607,20393,24555,23485,27454,21281,31568,29378,26694,30719,30518,26103,20917,20111,30420,23743,31397,33909,22862,39745,20608,32350,32351,32352,32353,32354,32355,32356,32357,32358,32359,32360,32361,32362,32363,32364,32365,32366,32367,32368,32369,32370,32371,32372,32373,32374,32375,32376,32377,32378,32379,32380,32381,32382,32383,32384,32385,32387,32388,32389,32390,32391,32392,32393,32394,32395,32396,32397,32398,32399,32400,32401,32402,32403,32404,32405,32406,32407,32408,32409,32410,32412,32413,32414,32430,32436,32443,32444,32470,32484,32492,32505,32522,32528,32542,32567,32569,32571,32572,32573,32574,32575,32576,32577,32579,32582,32583,32584,32585,32586,32587,32588,32589,32590,32591,32594,32595,39304,24871,28291,22372,26118,25414,22256,25324,25193,24275,38420,22403,25289,21895,34593,33098,36771,21862,33713,26469,36182,34013,23146,26639,25318,31726,38417,20848,28572,35888,25597,35272,25042,32518,28866,28389,29701,27028,29436,24266,37070,26391,28010,25438,21171,29282,32769,20332,23013,37226,28889,28061,21202,20048,38647,38253,34174,30922,32047,20769,22418,25794,32907,31867,27882,26865,26974,20919,21400,26792,29313,40654,31729,29432,31163,28435,29702,26446,37324,40100,31036,33673,33620,21519,26647,20029,21385,21169,30782,21382,21033,20616,20363,20432,32598,32601,32603,32604,32605,32606,32608,32611,32612,32613,32614,32615,32619,32620,32621,32623,32624,32627,32629,32630,32631,32632,32634,32635,32636,32637,32639,32640,32642,32643,32644,32645,32646,32647,32648,32649,32651,32653,32655,32656,32657,32658,32659,32661,32662,32663,32664,32665,32667,32668,32672,32674,32675,32677,32678,32680,32681,32682,32683,32684,32685,32686,32689,32691,32692,32693,32694,32695,32698,32699,32702,32704,32706,32707,32708,32710,32711,32712,32713,32715,32717,32719,32720,32721,32722,32723,32726,32727,32729,32730,32731,32732,32733,32734,32738,32739,30178,31435,31890,27813,38582,21147,29827,21737,20457,32852,33714,36830,38256,24265,24604,28063,24088,25947,33080,38142,24651,28860,32451,31918,20937,26753,31921,33391,20004,36742,37327,26238,20142,35845,25769,32842,20698,30103,29134,23525,36797,28518,20102,25730,38243,24278,26009,21015,35010,28872,21155,29454,29747,26519,30967,38678,20020,37051,40158,28107,20955,36161,21533,25294,29618,33777,38646,40836,38083,20278,32666,20940,28789,38517,23725,39046,21478,20196,28316,29705,27060,30827,39311,30041,21016,30244,27969,26611,20845,40857,32843,21657,31548,31423,32740,32743,32744,32746,32747,32748,32749,32751,32754,32756,32757,32758,32759,32760,32761,32762,32765,32766,32767,32770,32775,32776,32777,32778,32782,32783,32785,32787,32794,32795,32797,32798,32799,32801,32803,32804,32811,32812,32813,32814,32815,32816,32818,32820,32825,32826,32828,32830,32832,32833,32836,32837,32839,32840,32841,32846,32847,32848,32849,32851,32853,32854,32855,32857,32859,32860,32861,32862,32863,32864,32865,32866,32867,32868,32869,32870,32871,32872,32875,32876,32877,32878,32879,32880,32882,32883,32884,32885,32886,32887,32888,32889,32890,32891,32892,32893,38534,22404,25314,38471,27004,23044,25602,31699,28431,38475,33446,21346,39045,24208,28809,25523,21348,34383,40065,40595,30860,38706,36335,36162,40575,28510,31108,24405,38470,25134,39540,21525,38109,20387,26053,23653,23649,32533,34385,27695,24459,29575,28388,32511,23782,25371,23402,28390,21365,20081,25504,30053,25249,36718,20262,20177,27814,32438,35770,33821,34746,32599,36923,38179,31657,39585,35064,33853,27931,39558,32476,22920,40635,29595,30721,34434,39532,39554,22043,21527,22475,20080,40614,21334,36808,33033,30610,39314,34542,28385,34067,26364,24930,28459,32894,32897,32898,32901,32904,32906,32909,32910,32911,32912,32913,32914,32916,32917,32919,32921,32926,32931,32934,32935,32936,32940,32944,32947,32949,32950,32952,32953,32955,32965,32967,32968,32969,32970,32971,32975,32976,32977,32978,32979,32980,32981,32984,32991,32992,32994,32995,32998,33006,33013,33015,33017,33019,33022,33023,33024,33025,33027,33028,33029,33031,33032,33035,33036,33045,33047,33049,33051,33052,33053,33055,33056,33057,33058,33059,33060,33061,33062,33063,33064,33065,33066,33067,33069,33070,33072,33075,33076,33077,33079,33081,33082,33083,33084,33085,33087,35881,33426,33579,30450,27667,24537,33725,29483,33541,38170,27611,30683,38086,21359,33538,20882,24125,35980,36152,20040,29611,26522,26757,37238,38665,29028,27809,30473,23186,38209,27599,32654,26151,23504,22969,23194,38376,38391,20204,33804,33945,27308,30431,38192,29467,26790,23391,30511,37274,38753,31964,36855,35868,24357,31859,31192,35269,27852,34588,23494,24130,26825,30496,32501,20885,20813,21193,23081,32517,38754,33495,25551,30596,34256,31186,28218,24217,22937,34065,28781,27665,25279,30399,25935,24751,38397,26126,34719,40483,38125,21517,21629,35884,25720,33088,33089,33090,33091,33092,33093,33095,33097,33101,33102,33103,33106,33110,33111,33112,33115,33116,33117,33118,33119,33121,33122,33123,33124,33126,33128,33130,33131,33132,33135,33138,33139,33141,33142,33143,33144,33153,33155,33156,33157,33158,33159,33161,33163,33164,33165,33166,33168,33170,33171,33172,33173,33174,33175,33177,33178,33182,33183,33184,33185,33186,33188,33189,33191,33193,33195,33196,33197,33198,33199,33200,33201,33202,33204,33205,33206,33207,33208,33209,33212,33213,33214,33215,33220,33221,33223,33224,33225,33227,33229,33230,33231,33232,33233,33234,33235,25721,34321,27169,33180,30952,25705,39764,25273,26411,33707,22696,40664,27819,28448,23518,38476,35851,29279,26576,25287,29281,20137,22982,27597,22675,26286,24149,21215,24917,26408,30446,30566,29287,31302,25343,21738,21584,38048,37027,23068,32435,27670,20035,22902,32784,22856,21335,30007,38590,22218,25376,33041,24700,38393,28118,21602,39297,20869,23273,33021,22958,38675,20522,27877,23612,25311,20320,21311,33147,36870,28346,34091,25288,24180,30910,25781,25467,24565,23064,37247,40479,23615,25423,32834,23421,21870,38218,38221,28037,24744,26592,29406,20957,23425,33236,33237,33238,33239,33240,33241,33242,33243,33244,33245,33246,33247,33248,33249,33250,33252,33253,33254,33256,33257,33259,33262,33263,33264,33265,33266,33269,33270,33271,33272,33273,33274,33277,33279,33283,33287,33288,33289,33290,33291,33294,33295,33297,33299,33301,33302,33303,33304,33305,33306,33309,33312,33316,33317,33318,33319,33321,33326,33330,33338,33340,33341,33343,33344,33345,33346,33347,33349,33350,33352,33354,33356,33357,33358,33360,33361,33362,33363,33364,33365,33366,33367,33369,33371,33372,33373,33374,33376,33377,33378,33379,33380,33381,33382,33383,33385,25319,27870,29275,25197,38062,32445,33043,27987,20892,24324,22900,21162,24594,22899,26262,34384,30111,25386,25062,31983,35834,21734,27431,40485,27572,34261,21589,20598,27812,21866,36276,29228,24085,24597,29750,25293,25490,29260,24472,28227,27966,25856,28504,30424,30928,30460,30036,21028,21467,20051,24222,26049,32810,32982,25243,21638,21032,28846,34957,36305,27873,21624,32986,22521,35060,36180,38506,37197,20329,27803,21943,30406,30768,25256,28921,28558,24429,34028,26842,30844,31735,33192,26379,40527,25447,30896,22383,30738,38713,25209,25259,21128,29749,27607,33386,33387,33388,33389,33393,33397,33398,33399,33400,33403,33404,33408,33409,33411,33413,33414,33415,33417,33420,33424,33427,33428,33429,33430,33434,33435,33438,33440,33442,33443,33447,33458,33461,33462,33466,33467,33468,33471,33472,33474,33475,33477,33478,33481,33488,33494,33497,33498,33501,33506,33511,33512,33513,33514,33516,33517,33518,33520,33522,33523,33525,33526,33528,33530,33532,33533,33534,33535,33536,33546,33547,33549,33552,33554,33555,33558,33560,33561,33565,33566,33567,33568,33569,33570,33571,33572,33573,33574,33577,33578,33582,33584,33586,33591,33595,33597,21860,33086,30130,30382,21305,30174,20731,23617,35692,31687,20559,29255,39575,39128,28418,29922,31080,25735,30629,25340,39057,36139,21697,32856,20050,22378,33529,33805,24179,20973,29942,35780,23631,22369,27900,39047,23110,30772,39748,36843,31893,21078,25169,38138,20166,33670,33889,33769,33970,22484,26420,22275,26222,28006,35889,26333,28689,26399,27450,26646,25114,22971,19971,20932,28422,26578,27791,20854,26827,22855,27495,30054,23822,33040,40784,26071,31048,31041,39569,36215,23682,20062,20225,21551,22865,30732,22120,27668,36804,24323,27773,27875,35755,25488,33598,33599,33601,33602,33604,33605,33608,33610,33611,33612,33613,33614,33619,33621,33622,33623,33624,33625,33629,33634,33648,33649,33650,33651,33652,33653,33654,33657,33658,33662,33663,33664,33665,33666,33667,33668,33671,33672,33674,33675,33676,33677,33679,33680,33681,33684,33685,33686,33687,33689,33690,33693,33695,33697,33698,33699,33700,33701,33702,33703,33708,33709,33710,33711,33717,33723,33726,33727,33730,33731,33732,33734,33736,33737,33739,33741,33742,33744,33745,33746,33747,33749,33751,33753,33754,33755,33758,33762,33763,33764,33766,33767,33768,33771,33772,33773,24688,27965,29301,25190,38030,38085,21315,36801,31614,20191,35878,20094,40660,38065,38067,21069,28508,36963,27973,35892,22545,23884,27424,27465,26538,21595,33108,32652,22681,34103,24378,25250,27207,38201,25970,24708,26725,30631,20052,20392,24039,38808,25772,32728,23789,20431,31373,20999,33540,19988,24623,31363,38054,20405,20146,31206,29748,21220,33465,25810,31165,23517,27777,38738,36731,27682,20542,21375,28165,25806,26228,27696,24773,39031,35831,24198,29756,31351,31179,19992,37041,29699,27714,22234,37195,27845,36235,21306,34502,26354,36527,23624,39537,28192,33774,33775,33779,33780,33781,33782,33783,33786,33787,33788,33790,33791,33792,33794,33797,33799,33800,33801,33802,33808,33810,33811,33812,33813,33814,33815,33817,33818,33819,33822,33823,33824,33825,33826,33827,33833,33834,33835,33836,33837,33838,33839,33840,33842,33843,33844,33845,33846,33847,33849,33850,33851,33854,33855,33856,33857,33858,33859,33860,33861,33863,33864,33865,33866,33867,33868,33869,33870,33871,33872,33874,33875,33876,33877,33878,33880,33885,33886,33887,33888,33890,33892,33893,33894,33895,33896,33898,33902,33903,33904,33906,33908,33911,33913,33915,33916,21462,23094,40843,36259,21435,22280,39079,26435,37275,27849,20840,30154,25331,29356,21048,21149,32570,28820,30264,21364,40522,27063,30830,38592,35033,32676,28982,29123,20873,26579,29924,22756,25880,22199,35753,39286,25200,32469,24825,28909,22764,20161,20154,24525,38887,20219,35748,20995,22922,32427,25172,20173,26085,25102,33592,33993,33635,34701,29076,28342,23481,32466,20887,25545,26580,32905,33593,34837,20754,23418,22914,36785,20083,27741,20837,35109,36719,38446,34122,29790,38160,38384,28070,33509,24369,25746,27922,33832,33134,40131,22622,36187,19977,21441,33917,33918,33919,33920,33921,33923,33924,33925,33926,33930,33933,33935,33936,33937,33938,33939,33940,33941,33942,33944,33946,33947,33949,33950,33951,33952,33954,33955,33956,33957,33958,33959,33960,33961,33962,33963,33964,33965,33966,33968,33969,33971,33973,33974,33975,33979,33980,33982,33984,33986,33987,33989,33990,33991,33992,33995,33996,33998,33999,34002,34004,34005,34007,34008,34009,34010,34011,34012,34014,34017,34018,34020,34023,34024,34025,34026,34027,34029,34030,34031,34033,34034,34035,34036,34037,34038,34039,34040,34041,34042,34043,34045,34046,34048,34049,34050,20254,25955,26705,21971,20007,25620,39578,25195,23234,29791,33394,28073,26862,20711,33678,30722,26432,21049,27801,32433,20667,21861,29022,31579,26194,29642,33515,26441,23665,21024,29053,34923,38378,38485,25797,36193,33203,21892,27733,25159,32558,22674,20260,21830,36175,26188,19978,23578,35059,26786,25422,31245,28903,33421,21242,38902,23569,21736,37045,32461,22882,36170,34503,33292,33293,36198,25668,23556,24913,28041,31038,35774,30775,30003,21627,20280,36523,28145,23072,32453,31070,27784,23457,23158,29978,32958,24910,28183,22768,29983,29989,29298,21319,32499,34051,34052,34053,34054,34055,34056,34057,34058,34059,34061,34062,34063,34064,34066,34068,34069,34070,34072,34073,34075,34076,34077,34078,34080,34082,34083,34084,34085,34086,34087,34088,34089,34090,34093,34094,34095,34096,34097,34098,34099,34100,34101,34102,34110,34111,34112,34113,34114,34116,34117,34118,34119,34123,34124,34125,34126,34127,34128,34129,34130,34131,34132,34133,34135,34136,34138,34139,34140,34141,34143,34144,34145,34146,34147,34149,34150,34151,34153,34154,34155,34156,34157,34158,34159,34160,34161,34163,34165,34166,34167,34168,34172,34173,34175,34176,34177,30465,30427,21097,32988,22307,24072,22833,29422,26045,28287,35799,23608,34417,21313,30707,25342,26102,20160,39135,34432,23454,35782,21490,30690,20351,23630,39542,22987,24335,31034,22763,19990,26623,20107,25325,35475,36893,21183,26159,21980,22124,36866,20181,20365,37322,39280,27663,24066,24643,23460,35270,35797,25910,25163,39318,23432,23551,25480,21806,21463,30246,20861,34092,26530,26803,27530,25234,36755,21460,33298,28113,30095,20070,36174,23408,29087,34223,26257,26329,32626,34560,40653,40736,23646,26415,36848,26641,26463,25101,31446,22661,24246,25968,28465,34178,34179,34182,34184,34185,34186,34187,34188,34189,34190,34192,34193,34194,34195,34196,34197,34198,34199,34200,34201,34202,34205,34206,34207,34208,34209,34210,34211,34213,34214,34215,34217,34219,34220,34221,34225,34226,34227,34228,34229,34230,34232,34234,34235,34236,34237,34238,34239,34240,34242,34243,34244,34245,34246,34247,34248,34250,34251,34252,34253,34254,34257,34258,34260,34262,34263,34264,34265,34266,34267,34269,34270,34271,34272,34273,34274,34275,34277,34278,34279,34280,34282,34283,34284,34285,34286,34287,34288,34289,34290,34291,34292,34293,34294,34295,34296,24661,21047,32781,25684,34928,29993,24069,26643,25332,38684,21452,29245,35841,27700,30561,31246,21550,30636,39034,33308,35828,30805,26388,28865,26031,25749,22070,24605,31169,21496,19997,27515,32902,23546,21987,22235,20282,20284,39282,24051,26494,32824,24578,39042,36865,23435,35772,35829,25628,33368,25822,22013,33487,37221,20439,32032,36895,31903,20723,22609,28335,23487,35785,32899,37240,33948,31639,34429,38539,38543,32485,39635,30862,23681,31319,36930,38567,31071,23385,25439,31499,34001,26797,21766,32553,29712,32034,38145,25152,22604,20182,23427,22905,22612,34297,34298,34300,34301,34302,34304,34305,34306,34307,34308,34310,34311,34312,34313,34314,34315,34316,34317,34318,34319,34320,34322,34323,34324,34325,34327,34328,34329,34330,34331,34332,34333,34334,34335,34336,34337,34338,34339,34340,34341,34342,34344,34346,34347,34348,34349,34350,34351,34352,34353,34354,34355,34356,34357,34358,34359,34361,34362,34363,34365,34366,34367,34368,34369,34370,34371,34372,34373,34374,34375,34376,34377,34378,34379,34380,34386,34387,34389,34390,34391,34392,34393,34395,34396,34397,34399,34400,34401,34403,34404,34405,34406,34407,34408,34409,34410,29549,25374,36427,36367,32974,33492,25260,21488,27888,37214,22826,24577,27760,22349,25674,36138,30251,28393,22363,27264,30192,28525,35885,35848,22374,27631,34962,30899,25506,21497,28845,27748,22616,25642,22530,26848,33179,21776,31958,20504,36538,28108,36255,28907,25487,28059,28372,32486,33796,26691,36867,28120,38518,35752,22871,29305,34276,33150,30140,35466,26799,21076,36386,38161,25552,39064,36420,21884,20307,26367,22159,24789,28053,21059,23625,22825,28155,22635,30000,29980,24684,33300,33094,25361,26465,36834,30522,36339,36148,38081,24086,21381,21548,28867,34413,34415,34416,34418,34419,34420,34421,34422,34423,34424,34435,34436,34437,34438,34439,34440,34441,34446,34447,34448,34449,34450,34452,34454,34455,34456,34457,34458,34459,34462,34463,34464,34465,34466,34469,34470,34475,34477,34478,34482,34483,34487,34488,34489,34491,34492,34493,34494,34495,34497,34498,34499,34501,34504,34508,34509,34514,34515,34517,34518,34519,34522,34524,34525,34528,34529,34530,34531,34533,34534,34535,34536,34538,34539,34540,34543,34549,34550,34551,34554,34555,34556,34557,34559,34561,34564,34565,34566,34571,34572,34574,34575,34576,34577,34580,34582,27712,24311,20572,20141,24237,25402,33351,36890,26704,37230,30643,21516,38108,24420,31461,26742,25413,31570,32479,30171,20599,25237,22836,36879,20984,31171,31361,22270,24466,36884,28034,23648,22303,21520,20820,28237,22242,25512,39059,33151,34581,35114,36864,21534,23663,33216,25302,25176,33073,40501,38464,39534,39548,26925,22949,25299,21822,25366,21703,34521,27964,23043,29926,34972,27498,22806,35916,24367,28286,29609,39037,20024,28919,23436,30871,25405,26202,30358,24779,23451,23113,19975,33109,27754,29579,20129,26505,32593,24448,26106,26395,24536,22916,23041,34585,34587,34589,34591,34592,34596,34598,34599,34600,34602,34603,34604,34605,34607,34608,34610,34611,34613,34614,34616,34617,34618,34620,34621,34624,34625,34626,34627,34628,34629,34630,34634,34635,34637,34639,34640,34641,34642,34644,34645,34646,34648,34650,34651,34652,34653,34654,34655,34657,34658,34662,34663,34664,34665,34666,34667,34668,34669,34671,34673,34674,34675,34677,34679,34680,34681,34682,34687,34688,34689,34692,34694,34695,34697,34698,34700,34702,34703,34704,34705,34706,34708,34709,34710,34712,34713,34714,34715,34716,34717,34718,34720,34721,34722,34723,34724,24013,24494,21361,38886,36829,26693,22260,21807,24799,20026,28493,32500,33479,33806,22996,20255,20266,23614,32428,26410,34074,21619,30031,32963,21890,39759,20301,28205,35859,23561,24944,21355,30239,28201,34442,25991,38395,32441,21563,31283,32010,38382,21985,32705,29934,25373,34583,28065,31389,25105,26017,21351,25569,27779,24043,21596,38056,20044,27745,35820,23627,26080,33436,26791,21566,21556,27595,27494,20116,25410,21320,33310,20237,20398,22366,25098,38654,26212,29289,21247,21153,24735,35823,26132,29081,26512,35199,30802,30717,26224,22075,21560,38177,29306,34725,34726,34727,34729,34730,34734,34736,34737,34738,34740,34742,34743,34744,34745,34747,34748,34750,34751,34753,34754,34755,34756,34757,34759,34760,34761,34764,34765,34766,34767,34768,34772,34773,34774,34775,34776,34777,34778,34780,34781,34782,34783,34785,34786,34787,34788,34790,34791,34792,34793,34795,34796,34797,34799,34800,34801,34802,34803,34804,34805,34806,34807,34808,34810,34811,34812,34813,34815,34816,34817,34818,34820,34821,34822,34823,34824,34825,34827,34828,34829,34830,34831,34832,34833,34834,34836,34839,34840,34841,34842,34844,34845,34846,34847,34848,34851,31232,24687,24076,24713,33181,22805,24796,29060,28911,28330,27728,29312,27268,34989,24109,20064,23219,21916,38115,27927,31995,38553,25103,32454,30606,34430,21283,38686,36758,26247,23777,20384,29421,19979,21414,22799,21523,25472,38184,20808,20185,40092,32420,21688,36132,34900,33335,38386,28046,24358,23244,26174,38505,29616,29486,21439,33146,39301,32673,23466,38519,38480,32447,30456,21410,38262,39321,31665,35140,28248,20065,32724,31077,35814,24819,21709,20139,39033,24055,27233,20687,21521,35937,33831,30813,38660,21066,21742,22179,38144,28040,23477,28102,26195,34852,34853,34854,34855,34856,34857,34858,34859,34860,34861,34862,34863,34864,34865,34867,34868,34869,34870,34871,34872,34874,34875,34877,34878,34879,34881,34882,34883,34886,34887,34888,34889,34890,34891,34894,34895,34896,34897,34898,34899,34901,34902,34904,34906,34907,34908,34909,34910,34911,34912,34918,34919,34922,34925,34927,34929,34931,34932,34933,34934,34936,34937,34938,34939,34940,34944,34947,34950,34951,34953,34954,34956,34958,34959,34960,34961,34963,34964,34965,34967,34968,34969,34970,34971,34973,34974,34975,34976,34977,34979,34981,34982,34983,34984,34985,34986,23567,23389,26657,32918,21880,31505,25928,26964,20123,27463,34638,38795,21327,25375,25658,37034,26012,32961,35856,20889,26800,21368,34809,25032,27844,27899,35874,23633,34218,33455,38156,27427,36763,26032,24571,24515,20449,34885,26143,33125,29481,24826,20852,21009,22411,24418,37026,34892,37266,24184,26447,24615,22995,20804,20982,33016,21256,27769,38596,29066,20241,20462,32670,26429,21957,38152,31168,34966,32483,22687,25100,38656,34394,22040,39035,24464,35768,33988,37207,21465,26093,24207,30044,24676,32110,23167,32490,32493,36713,21927,23459,24748,26059,29572,34988,34990,34991,34992,34994,34995,34996,34997,34998,35000,35001,35002,35003,35005,35006,35007,35008,35011,35012,35015,35016,35018,35019,35020,35021,35023,35024,35025,35027,35030,35031,35034,35035,35036,35037,35038,35040,35041,35046,35047,35049,35050,35051,35052,35053,35054,35055,35058,35061,35062,35063,35066,35067,35069,35071,35072,35073,35075,35076,35077,35078,35079,35080,35081,35083,35084,35085,35086,35087,35089,35092,35093,35094,35095,35096,35100,35101,35102,35103,35104,35106,35107,35108,35110,35111,35112,35113,35116,35117,35118,35119,35121,35122,35123,35125,35127,36873,30307,30505,32474,38772,34203,23398,31348,38634,34880,21195,29071,24490,26092,35810,23547,39535,24033,27529,27739,35757,35759,36874,36805,21387,25276,40486,40493,21568,20011,33469,29273,34460,23830,34905,28079,38597,21713,20122,35766,28937,21693,38409,28895,28153,30416,20005,30740,34578,23721,24310,35328,39068,38414,28814,27839,22852,25513,30524,34893,28436,33395,22576,29141,21388,30746,38593,21761,24422,28976,23476,35866,39564,27523,22830,40495,31207,26472,25196,20335,30113,32650,27915,38451,27687,20208,30162,20859,26679,28478,36992,33136,22934,29814,35128,35129,35130,35131,35132,35133,35134,35135,35136,35138,35139,35141,35142,35143,35144,35145,35146,35147,35148,35149,35150,35151,35152,35153,35154,35155,35156,35157,35158,35159,35160,35161,35162,35163,35164,35165,35168,35169,35170,35171,35172,35173,35175,35176,35177,35178,35179,35180,35181,35182,35183,35184,35185,35186,35187,35188,35189,35190,35191,35192,35193,35194,35196,35197,35198,35200,35202,35204,35205,35207,35208,35209,35210,35211,35212,35213,35214,35215,35216,35217,35218,35219,35220,35221,35222,35223,35224,35225,35226,35227,35228,35229,35230,35231,35232,35233,25671,23591,36965,31377,35875,23002,21676,33280,33647,35201,32768,26928,22094,32822,29239,37326,20918,20063,39029,25494,19994,21494,26355,33099,22812,28082,19968,22777,21307,25558,38129,20381,20234,34915,39056,22839,36951,31227,20202,33008,30097,27778,23452,23016,24413,26885,34433,20506,24050,20057,30691,20197,33402,25233,26131,37009,23673,20159,24441,33222,36920,32900,30123,20134,35028,24847,27589,24518,20041,30410,28322,35811,35758,35850,35793,24322,32764,32716,32462,33589,33643,22240,27575,38899,38452,23035,21535,38134,28139,23493,39278,23609,24341,38544,35234,35235,35236,35237,35238,35239,35240,35241,35242,35243,35244,35245,35246,35247,35248,35249,35250,35251,35252,35253,35254,35255,35256,35257,35258,35259,35260,35261,35262,35263,35264,35267,35277,35283,35284,35285,35287,35288,35289,35291,35293,35295,35296,35297,35298,35300,35303,35304,35305,35306,35308,35309,35310,35312,35313,35314,35316,35317,35318,35319,35320,35321,35322,35323,35324,35325,35326,35327,35329,35330,35331,35332,35333,35334,35336,35337,35338,35339,35340,35341,35342,35343,35344,35345,35346,35347,35348,35349,35350,35351,35352,35353,35354,35355,35356,35357,21360,33521,27185,23156,40560,24212,32552,33721,33828,33829,33639,34631,36814,36194,30408,24433,39062,30828,26144,21727,25317,20323,33219,30152,24248,38605,36362,34553,21647,27891,28044,27704,24703,21191,29992,24189,20248,24736,24551,23588,30001,37038,38080,29369,27833,28216,37193,26377,21451,21491,20305,37321,35825,21448,24188,36802,28132,20110,30402,27014,34398,24858,33286,20313,20446,36926,40060,24841,28189,28180,38533,20104,23089,38632,19982,23679,31161,23431,35821,32701,29577,22495,33419,37057,21505,36935,21947,23786,24481,24840,27442,29425,32946,35465,35358,35359,35360,35361,35362,35363,35364,35365,35366,35367,35368,35369,35370,35371,35372,35373,35374,35375,35376,35377,35378,35379,35380,35381,35382,35383,35384,35385,35386,35387,35388,35389,35391,35392,35393,35394,35395,35396,35397,35398,35399,35401,35402,35403,35404,35405,35406,35407,35408,35409,35410,35411,35412,35413,35414,35415,35416,35417,35418,35419,35420,35421,35422,35423,35424,35425,35426,35427,35428,35429,35430,35431,35432,35433,35434,35435,35436,35437,35438,35439,35440,35441,35442,35443,35444,35445,35446,35447,35448,35450,35451,35452,35453,35454,35455,35456,28020,23507,35029,39044,35947,39533,40499,28170,20900,20803,22435,34945,21407,25588,36757,22253,21592,22278,29503,28304,32536,36828,33489,24895,24616,38498,26352,32422,36234,36291,38053,23731,31908,26376,24742,38405,32792,20113,37095,21248,38504,20801,36816,34164,37213,26197,38901,23381,21277,30776,26434,26685,21705,28798,23472,36733,20877,22312,21681,25874,26242,36190,36163,33039,33900,36973,31967,20991,34299,26531,26089,28577,34468,36481,22122,36896,30338,28790,29157,36131,25321,21017,27901,36156,24590,22686,24974,26366,36192,25166,21939,28195,26413,36711,35457,35458,35459,35460,35461,35462,35463,35464,35467,35468,35469,35470,35471,35472,35473,35474,35476,35477,35478,35479,35480,35481,35482,35483,35484,35485,35486,35487,35488,35489,35490,35491,35492,35493,35494,35495,35496,35497,35498,35499,35500,35501,35502,35503,35504,35505,35506,35507,35508,35509,35510,35511,35512,35513,35514,35515,35516,35517,35518,35519,35520,35521,35522,35523,35524,35525,35526,35527,35528,35529,35530,35531,35532,35533,35534,35535,35536,35537,35538,35539,35540,35541,35542,35543,35544,35545,35546,35547,35548,35549,35550,35551,35552,35553,35554,35555,38113,38392,30504,26629,27048,21643,20045,28856,35784,25688,25995,23429,31364,20538,23528,30651,27617,35449,31896,27838,30415,26025,36759,23853,23637,34360,26632,21344,25112,31449,28251,32509,27167,31456,24432,28467,24352,25484,28072,26454,19976,24080,36134,20183,32960,30260,38556,25307,26157,25214,27836,36213,29031,32617,20806,32903,21484,36974,25240,21746,34544,36761,32773,38167,34071,36825,27993,29645,26015,30495,29956,30759,33275,36126,38024,20390,26517,30137,35786,38663,25391,38215,38453,33976,25379,30529,24449,29424,20105,24596,25972,25327,27491,25919,35556,35557,35558,35559,35560,35561,35562,35563,35564,35565,35566,35567,35568,35569,35570,35571,35572,35573,35574,35575,35576,35577,35578,35579,35580,35581,35582,35583,35584,35585,35586,35587,35588,35589,35590,35592,35593,35594,35595,35596,35597,35598,35599,35600,35601,35602,35603,35604,35605,35606,35607,35608,35609,35610,35611,35612,35613,35614,35615,35616,35617,35618,35619,35620,35621,35623,35624,35625,35626,35627,35628,35629,35630,35631,35632,35633,35634,35635,35636,35637,35638,35639,35640,35641,35642,35643,35644,35645,35646,35647,35648,35649,35650,35651,35652,35653,24103,30151,37073,35777,33437,26525,25903,21553,34584,30693,32930,33026,27713,20043,32455,32844,30452,26893,27542,25191,20540,20356,22336,25351,27490,36286,21482,26088,32440,24535,25370,25527,33267,33268,32622,24092,23769,21046,26234,31209,31258,36136,28825,30164,28382,27835,31378,20013,30405,24544,38047,34935,32456,31181,32959,37325,20210,20247,33311,21608,24030,27954,35788,31909,36724,32920,24090,21650,30385,23449,26172,39588,29664,26666,34523,26417,29482,35832,35803,36880,31481,28891,29038,25284,30633,22065,20027,33879,26609,21161,34496,36142,38136,31569,35654,35655,35656,35657,35658,35659,35660,35661,35662,35663,35664,35665,35666,35667,35668,35669,35670,35671,35672,35673,35674,35675,35676,35677,35678,35679,35680,35681,35682,35683,35684,35685,35687,35688,35689,35690,35691,35693,35694,35695,35696,35697,35698,35699,35700,35701,35702,35703,35704,35705,35706,35707,35708,35709,35710,35711,35712,35713,35714,35715,35716,35717,35718,35719,35720,35721,35722,35723,35724,35725,35726,35727,35728,35729,35730,35731,35732,35733,35734,35735,35736,35737,35738,35739,35740,35741,35742,35743,35756,35761,35771,35783,35792,35818,35849,35870,20303,27880,31069,39547,25235,29226,25341,19987,30742,36716,25776,36186,31686,26729,24196,35013,22918,25758,22766,29366,26894,38181,36861,36184,22368,32512,35846,20934,25417,25305,21331,26700,29730,33537,37196,21828,30528,28796,27978,20857,21672,36164,23039,28363,28100,23388,32043,20180,31869,28371,23376,33258,28173,23383,39683,26837,36394,23447,32508,24635,32437,37049,36208,22863,25549,31199,36275,21330,26063,31062,35781,38459,32452,38075,32386,22068,37257,26368,32618,23562,36981,26152,24038,20304,26590,20570,20316,22352,24231,59408,59409,59410,59411,59412,35896,35897,35898,35899,35900,35901,35902,35903,35904,35906,35907,35908,35909,35912,35914,35915,35917,35918,35919,35920,35921,35922,35923,35924,35926,35927,35928,35929,35931,35932,35933,35934,35935,35936,35939,35940,35941,35942,35943,35944,35945,35948,35949,35950,35951,35952,35953,35954,35956,35957,35958,35959,35963,35964,35965,35966,35967,35968,35969,35971,35972,35974,35975,35976,35979,35981,35982,35983,35984,35985,35986,35987,35989,35990,35991,35993,35994,35995,35996,35997,35998,35999,36000,36001,36002,36003,36004,36005,36006,36007,36008,36009,36010,36011,36012,36013,20109,19980,20800,19984,24319,21317,19989,20120,19998,39730,23404,22121,20008,31162,20031,21269,20039,22829,29243,21358,27664,22239,32996,39319,27603,30590,40727,20022,20127,40720,20060,20073,20115,33416,23387,21868,22031,20164,21389,21405,21411,21413,21422,38757,36189,21274,21493,21286,21294,21310,36188,21350,21347,20994,21000,21006,21037,21043,21055,21056,21068,21086,21089,21084,33967,21117,21122,21121,21136,21139,20866,32596,20155,20163,20169,20162,20200,20193,20203,20190,20251,20211,20258,20324,20213,20261,20263,20233,20267,20318,20327,25912,20314,20317,36014,36015,36016,36017,36018,36019,36020,36021,36022,36023,36024,36025,36026,36027,36028,36029,36030,36031,36032,36033,36034,36035,36036,36037,36038,36039,36040,36041,36042,36043,36044,36045,36046,36047,36048,36049,36050,36051,36052,36053,36054,36055,36056,36057,36058,36059,36060,36061,36062,36063,36064,36065,36066,36067,36068,36069,36070,36071,36072,36073,36074,36075,36076,36077,36078,36079,36080,36081,36082,36083,36084,36085,36086,36087,36088,36089,36090,36091,36092,36093,36094,36095,36096,36097,36098,36099,36100,36101,36102,36103,36104,36105,36106,36107,36108,36109,20319,20311,20274,20285,20342,20340,20369,20361,20355,20367,20350,20347,20394,20348,20396,20372,20454,20456,20458,20421,20442,20451,20444,20433,20447,20472,20521,20556,20467,20524,20495,20526,20525,20478,20508,20492,20517,20520,20606,20547,20565,20552,20558,20588,20603,20645,20647,20649,20666,20694,20742,20717,20716,20710,20718,20743,20747,20189,27709,20312,20325,20430,40864,27718,31860,20846,24061,40649,39320,20865,22804,21241,21261,35335,21264,20971,22809,20821,20128,20822,20147,34926,34980,20149,33044,35026,31104,23348,34819,32696,20907,20913,20925,20924,36110,36111,36112,36113,36114,36115,36116,36117,36118,36119,36120,36121,36122,36123,36124,36128,36177,36178,36183,36191,36197,36200,36201,36202,36204,36206,36207,36209,36210,36216,36217,36218,36219,36220,36221,36222,36223,36224,36226,36227,36230,36231,36232,36233,36236,36237,36238,36239,36240,36242,36243,36245,36246,36247,36248,36249,36250,36251,36252,36253,36254,36256,36257,36258,36260,36261,36262,36263,36264,36265,36266,36267,36268,36269,36270,36271,36272,36274,36278,36279,36281,36283,36285,36288,36289,36290,36293,36295,36296,36297,36298,36301,36304,36306,36307,36308,20935,20886,20898,20901,35744,35750,35751,35754,35764,35765,35767,35778,35779,35787,35791,35790,35794,35795,35796,35798,35800,35801,35804,35807,35808,35812,35816,35817,35822,35824,35827,35830,35833,35836,35839,35840,35842,35844,35847,35852,35855,35857,35858,35860,35861,35862,35865,35867,35864,35869,35871,35872,35873,35877,35879,35882,35883,35886,35887,35890,35891,35893,35894,21353,21370,38429,38434,38433,38449,38442,38461,38460,38466,38473,38484,38495,38503,38508,38514,38516,38536,38541,38551,38576,37015,37019,37021,37017,37036,37025,37044,37043,37046,37050,36309,36312,36313,36316,36320,36321,36322,36325,36326,36327,36329,36333,36334,36336,36337,36338,36340,36342,36348,36350,36351,36352,36353,36354,36355,36356,36358,36359,36360,36363,36365,36366,36368,36369,36370,36371,36373,36374,36375,36376,36377,36378,36379,36380,36384,36385,36388,36389,36390,36391,36392,36395,36397,36400,36402,36403,36404,36406,36407,36408,36411,36412,36414,36415,36419,36421,36422,36428,36429,36430,36431,36432,36435,36436,36437,36438,36439,36440,36442,36443,36444,36445,36446,36447,36448,36449,36450,36451,36452,36453,36455,36456,36458,36459,36462,36465,37048,37040,37071,37061,37054,37072,37060,37063,37075,37094,37090,37084,37079,37083,37099,37103,37118,37124,37154,37150,37155,37169,37167,37177,37187,37190,21005,22850,21154,21164,21165,21182,21759,21200,21206,21232,21471,29166,30669,24308,20981,20988,39727,21430,24321,30042,24047,22348,22441,22433,22654,22716,22725,22737,22313,22316,22314,22323,22329,22318,22319,22364,22331,22338,22377,22405,22379,22406,22396,22395,22376,22381,22390,22387,22445,22436,22412,22450,22479,22439,22452,22419,22432,22485,22488,22490,22489,22482,22456,22516,22511,22520,22500,22493,36467,36469,36471,36472,36473,36474,36475,36477,36478,36480,36482,36483,36484,36486,36488,36489,36490,36491,36492,36493,36494,36497,36498,36499,36501,36502,36503,36504,36505,36506,36507,36509,36511,36512,36513,36514,36515,36516,36517,36518,36519,36520,36521,36522,36525,36526,36528,36529,36531,36532,36533,36534,36535,36536,36537,36539,36540,36541,36542,36543,36544,36545,36546,36547,36548,36549,36550,36551,36552,36553,36554,36555,36556,36557,36559,36560,36561,36562,36563,36564,36565,36566,36567,36568,36569,36570,36571,36572,36573,36574,36575,36576,36577,36578,36579,36580,22539,22541,22525,22509,22528,22558,22553,22596,22560,22629,22636,22657,22665,22682,22656,39336,40729,25087,33401,33405,33407,33423,33418,33448,33412,33422,33425,33431,33433,33451,33464,33470,33456,33480,33482,33507,33432,33463,33454,33483,33484,33473,33449,33460,33441,33450,33439,33476,33486,33444,33505,33545,33527,33508,33551,33543,33500,33524,33490,33496,33548,33531,33491,33553,33562,33542,33556,33557,33504,33493,33564,33617,33627,33628,33544,33682,33596,33588,33585,33691,33630,33583,33615,33607,33603,33631,33600,33559,33632,33581,33594,33587,33638,33637,36581,36582,36583,36584,36585,36586,36587,36588,36589,36590,36591,36592,36593,36594,36595,36596,36597,36598,36599,36600,36601,36602,36603,36604,36605,36606,36607,36608,36609,36610,36611,36612,36613,36614,36615,36616,36617,36618,36619,36620,36621,36622,36623,36624,36625,36626,36627,36628,36629,36630,36631,36632,36633,36634,36635,36636,36637,36638,36639,36640,36641,36642,36643,36644,36645,36646,36647,36648,36649,36650,36651,36652,36653,36654,36655,36656,36657,36658,36659,36660,36661,36662,36663,36664,36665,36666,36667,36668,36669,36670,36671,36672,36673,36674,36675,36676,33640,33563,33641,33644,33642,33645,33646,33712,33656,33715,33716,33696,33706,33683,33692,33669,33660,33718,33705,33661,33720,33659,33688,33694,33704,33722,33724,33729,33793,33765,33752,22535,33816,33803,33757,33789,33750,33820,33848,33809,33798,33748,33759,33807,33795,33784,33785,33770,33733,33728,33830,33776,33761,33884,33873,33882,33881,33907,33927,33928,33914,33929,33912,33852,33862,33897,33910,33932,33934,33841,33901,33985,33997,34000,34022,33981,34003,33994,33983,33978,34016,33953,33977,33972,33943,34021,34019,34060,29965,34104,34032,34105,34079,34106,36677,36678,36679,36680,36681,36682,36683,36684,36685,36686,36687,36688,36689,36690,36691,36692,36693,36694,36695,36696,36697,36698,36699,36700,36701,36702,36703,36704,36705,36706,36707,36708,36709,36714,36736,36748,36754,36765,36768,36769,36770,36772,36773,36774,36775,36778,36780,36781,36782,36783,36786,36787,36788,36789,36791,36792,36794,36795,36796,36799,36800,36803,36806,36809,36810,36811,36812,36813,36815,36818,36822,36823,36826,36832,36833,36835,36839,36844,36847,36849,36850,36852,36853,36854,36858,36859,36860,36862,36863,36871,36872,36876,36878,36883,36885,36888,34134,34107,34047,34044,34137,34120,34152,34148,34142,34170,30626,34115,34162,34171,34212,34216,34183,34191,34169,34222,34204,34181,34233,34231,34224,34259,34241,34268,34303,34343,34309,34345,34326,34364,24318,24328,22844,22849,32823,22869,22874,22872,21263,23586,23589,23596,23604,25164,25194,25247,25275,25290,25306,25303,25326,25378,25334,25401,25419,25411,25517,25590,25457,25466,25486,25524,25453,25516,25482,25449,25518,25532,25586,25592,25568,25599,25540,25566,25550,25682,25542,25534,25669,25665,25611,25627,25632,25612,25638,25633,25694,25732,25709,25750,36889,36892,36899,36900,36901,36903,36904,36905,36906,36907,36908,36912,36913,36914,36915,36916,36919,36921,36922,36925,36927,36928,36931,36933,36934,36936,36937,36938,36939,36940,36942,36948,36949,36950,36953,36954,36956,36957,36958,36959,36960,36961,36964,36966,36967,36969,36970,36971,36972,36975,36976,36977,36978,36979,36982,36983,36984,36985,36986,36987,36988,36990,36993,36996,36997,36998,36999,37001,37002,37004,37005,37006,37007,37008,37010,37012,37014,37016,37018,37020,37022,37023,37024,37028,37029,37031,37032,37033,37035,37037,37042,37047,37052,37053,37055,37056,25722,25783,25784,25753,25786,25792,25808,25815,25828,25826,25865,25893,25902,24331,24530,29977,24337,21343,21489,21501,21481,21480,21499,21522,21526,21510,21579,21586,21587,21588,21590,21571,21537,21591,21593,21539,21554,21634,21652,21623,21617,21604,21658,21659,21636,21622,21606,21661,21712,21677,21698,21684,21714,21671,21670,21715,21716,21618,21667,21717,21691,21695,21708,21721,21722,21724,21673,21674,21668,21725,21711,21726,21787,21735,21792,21757,21780,21747,21794,21795,21775,21777,21799,21802,21863,21903,21941,21833,21869,21825,21845,21823,21840,21820,37058,37059,37062,37064,37065,37067,37068,37069,37074,37076,37077,37078,37080,37081,37082,37086,37087,37088,37091,37092,37093,37097,37098,37100,37102,37104,37105,37106,37107,37109,37110,37111,37113,37114,37115,37116,37119,37120,37121,37123,37125,37126,37127,37128,37129,37130,37131,37132,37133,37134,37135,37136,37137,37138,37139,37140,37141,37142,37143,37144,37146,37147,37148,37149,37151,37152,37153,37156,37157,37158,37159,37160,37161,37162,37163,37164,37165,37166,37168,37170,37171,37172,37173,37174,37175,37176,37178,37179,37180,37181,37182,37183,37184,37185,37186,37188,21815,21846,21877,21878,21879,21811,21808,21852,21899,21970,21891,21937,21945,21896,21889,21919,21886,21974,21905,21883,21983,21949,21950,21908,21913,21994,22007,21961,22047,21969,21995,21996,21972,21990,21981,21956,21999,21989,22002,22003,21964,21965,21992,22005,21988,36756,22046,22024,22028,22017,22052,22051,22014,22016,22055,22061,22104,22073,22103,22060,22093,22114,22105,22108,22092,22100,22150,22116,22129,22123,22139,22140,22149,22163,22191,22228,22231,22237,22241,22261,22251,22265,22271,22276,22282,22281,22300,24079,24089,24084,24081,24113,24123,24124,37189,37191,37192,37201,37203,37204,37205,37206,37208,37209,37211,37212,37215,37216,37222,37223,37224,37227,37229,37235,37242,37243,37244,37248,37249,37250,37251,37252,37254,37256,37258,37262,37263,37267,37268,37269,37270,37271,37272,37273,37276,37277,37278,37279,37280,37281,37284,37285,37286,37287,37288,37289,37291,37292,37296,37297,37298,37299,37302,37303,37304,37305,37307,37308,37309,37310,37311,37312,37313,37314,37315,37316,37317,37318,37320,37323,37328,37330,37331,37332,37333,37334,37335,37336,37337,37338,37339,37341,37342,37343,37344,37345,37346,37347,37348,37349,24119,24132,24148,24155,24158,24161,23692,23674,23693,23696,23702,23688,23704,23705,23697,23706,23708,23733,23714,23741,23724,23723,23729,23715,23745,23735,23748,23762,23780,23755,23781,23810,23811,23847,23846,23854,23844,23838,23814,23835,23896,23870,23860,23869,23916,23899,23919,23901,23915,23883,23882,23913,23924,23938,23961,23965,35955,23991,24005,24435,24439,24450,24455,24457,24460,24469,24473,24476,24488,24493,24501,24508,34914,24417,29357,29360,29364,29367,29368,29379,29377,29390,29389,29394,29416,29423,29417,29426,29428,29431,29441,29427,29443,29434,37350,37351,37352,37353,37354,37355,37356,37357,37358,37359,37360,37361,37362,37363,37364,37365,37366,37367,37368,37369,37370,37371,37372,37373,37374,37375,37376,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37387,37388,37389,37390,37391,37392,37393,37394,37395,37396,37397,37398,37399,37400,37401,37402,37403,37404,37405,37406,37407,37408,37409,37410,37411,37412,37413,37414,37415,37416,37417,37418,37419,37420,37421,37422,37423,37424,37425,37426,37427,37428,37429,37430,37431,37432,37433,37434,37435,37436,37437,37438,37439,37440,37441,37442,37443,37444,37445,29435,29463,29459,29473,29450,29470,29469,29461,29474,29497,29477,29484,29496,29489,29520,29517,29527,29536,29548,29551,29566,33307,22821,39143,22820,22786,39267,39271,39272,39273,39274,39275,39276,39284,39287,39293,39296,39300,39303,39306,39309,39312,39313,39315,39316,39317,24192,24209,24203,24214,24229,24224,24249,24245,24254,24243,36179,24274,24273,24283,24296,24298,33210,24516,24521,24534,24527,24579,24558,24580,24545,24548,24574,24581,24582,24554,24557,24568,24601,24629,24614,24603,24591,24589,24617,24619,24586,24639,24609,24696,24697,24699,24698,24642,37446,37447,37448,37449,37450,37451,37452,37453,37454,37455,37456,37457,37458,37459,37460,37461,37462,37463,37464,37465,37466,37467,37468,37469,37470,37471,37472,37473,37474,37475,37476,37477,37478,37479,37480,37481,37482,37483,37484,37485,37486,37487,37488,37489,37490,37491,37493,37494,37495,37496,37497,37498,37499,37500,37501,37502,37503,37504,37505,37506,37507,37508,37509,37510,37511,37512,37513,37514,37515,37516,37517,37519,37520,37521,37522,37523,37524,37525,37526,37527,37528,37529,37530,37531,37532,37533,37534,37535,37536,37537,37538,37539,37540,37541,37542,37543,24682,24701,24726,24730,24749,24733,24707,24722,24716,24731,24812,24763,24753,24797,24792,24774,24794,24756,24864,24870,24853,24867,24820,24832,24846,24875,24906,24949,25004,24980,24999,25015,25044,25077,24541,38579,38377,38379,38385,38387,38389,38390,38396,38398,38403,38404,38406,38408,38410,38411,38412,38413,38415,38418,38421,38422,38423,38425,38426,20012,29247,25109,27701,27732,27740,27722,27811,27781,27792,27796,27788,27752,27753,27764,27766,27782,27817,27856,27860,27821,27895,27896,27889,27863,27826,27872,27862,27898,27883,27886,27825,27859,27887,27902,37544,37545,37546,37547,37548,37549,37551,37552,37553,37554,37555,37556,37557,37558,37559,37560,37561,37562,37563,37564,37565,37566,37567,37568,37569,37570,37571,37572,37573,37574,37575,37577,37578,37579,37580,37581,37582,37583,37584,37585,37586,37587,37588,37589,37590,37591,37592,37593,37594,37595,37596,37597,37598,37599,37600,37601,37602,37603,37604,37605,37606,37607,37608,37609,37610,37611,37612,37613,37614,37615,37616,37617,37618,37619,37620,37621,37622,37623,37624,37625,37626,37627,37628,37629,37630,37631,37632,37633,37634,37635,37636,37637,37638,37639,37640,37641,27961,27943,27916,27971,27976,27911,27908,27929,27918,27947,27981,27950,27957,27930,27983,27986,27988,27955,28049,28015,28062,28064,27998,28051,28052,27996,28000,28028,28003,28186,28103,28101,28126,28174,28095,28128,28177,28134,28125,28121,28182,28075,28172,28078,28203,28270,28238,28267,28338,28255,28294,28243,28244,28210,28197,28228,28383,28337,28312,28384,28461,28386,28325,28327,28349,28347,28343,28375,28340,28367,28303,28354,28319,28514,28486,28487,28452,28437,28409,28463,28470,28491,28532,28458,28425,28457,28553,28557,28556,28536,28530,28540,28538,28625,37642,37643,37644,37645,37646,37647,37648,37649,37650,37651,37652,37653,37654,37655,37656,37657,37658,37659,37660,37661,37662,37663,37664,37665,37666,37667,37668,37669,37670,37671,37672,37673,37674,37675,37676,37677,37678,37679,37680,37681,37682,37683,37684,37685,37686,37687,37688,37689,37690,37691,37692,37693,37695,37696,37697,37698,37699,37700,37701,37702,37703,37704,37705,37706,37707,37708,37709,37710,37711,37712,37713,37714,37715,37716,37717,37718,37719,37720,37721,37722,37723,37724,37725,37726,37727,37728,37729,37730,37731,37732,37733,37734,37735,37736,37737,37739,28617,28583,28601,28598,28610,28641,28654,28638,28640,28655,28698,28707,28699,28729,28725,28751,28766,23424,23428,23445,23443,23461,23480,29999,39582,25652,23524,23534,35120,23536,36423,35591,36790,36819,36821,36837,36846,36836,36841,36838,36851,36840,36869,36868,36875,36902,36881,36877,36886,36897,36917,36918,36909,36911,36932,36945,36946,36944,36968,36952,36962,36955,26297,36980,36989,36994,37000,36995,37003,24400,24407,24406,24408,23611,21675,23632,23641,23409,23651,23654,32700,24362,24361,24365,33396,24380,39739,23662,22913,22915,22925,22953,22954,22947,37740,37741,37742,37743,37744,37745,37746,37747,37748,37749,37750,37751,37752,37753,37754,37755,37756,37757,37758,37759,37760,37761,37762,37763,37764,37765,37766,37767,37768,37769,37770,37771,37772,37773,37774,37776,37777,37778,37779,37780,37781,37782,37783,37784,37785,37786,37787,37788,37789,37790,37791,37792,37793,37794,37795,37796,37797,37798,37799,37800,37801,37802,37803,37804,37805,37806,37807,37808,37809,37810,37811,37812,37813,37814,37815,37816,37817,37818,37819,37820,37821,37822,37823,37824,37825,37826,37827,37828,37829,37830,37831,37832,37833,37835,37836,37837,22935,22986,22955,22942,22948,22994,22962,22959,22999,22974,23045,23046,23005,23048,23011,23000,23033,23052,23049,23090,23092,23057,23075,23059,23104,23143,23114,23125,23100,23138,23157,33004,23210,23195,23159,23162,23230,23275,23218,23250,23252,23224,23264,23267,23281,23254,23270,23256,23260,23305,23319,23318,23346,23351,23360,23573,23580,23386,23397,23411,23377,23379,23394,39541,39543,39544,39546,39551,39549,39552,39553,39557,39560,39562,39568,39570,39571,39574,39576,39579,39580,39581,39583,39584,39586,39587,39589,39591,32415,32417,32419,32421,32424,32425,37838,37839,37840,37841,37842,37843,37844,37845,37847,37848,37849,37850,37851,37852,37853,37854,37855,37856,37857,37858,37859,37860,37861,37862,37863,37864,37865,37866,37867,37868,37869,37870,37871,37872,37873,37874,37875,37876,37877,37878,37879,37880,37881,37882,37883,37884,37885,37886,37887,37888,37889,37890,37891,37892,37893,37894,37895,37896,37897,37898,37899,37900,37901,37902,37903,37904,37905,37906,37907,37908,37909,37910,37911,37912,37913,37914,37915,37916,37917,37918,37919,37920,37921,37922,37923,37924,37925,37926,37927,37928,37929,37930,37931,37932,37933,37934,32429,32432,32446,32448,32449,32450,32457,32459,32460,32464,32468,32471,32475,32480,32481,32488,32491,32494,32495,32497,32498,32525,32502,32506,32507,32510,32513,32514,32515,32519,32520,32523,32524,32527,32529,32530,32535,32537,32540,32539,32543,32545,32546,32547,32548,32549,32550,32551,32554,32555,32556,32557,32559,32560,32561,32562,32563,32565,24186,30079,24027,30014,37013,29582,29585,29614,29602,29599,29647,29634,29649,29623,29619,29632,29641,29640,29669,29657,39036,29706,29673,29671,29662,29626,29682,29711,29738,29787,29734,29733,29736,29744,29742,29740,37935,37936,37937,37938,37939,37940,37941,37942,37943,37944,37945,37946,37947,37948,37949,37951,37952,37953,37954,37955,37956,37957,37958,37959,37960,37961,37962,37963,37964,37965,37966,37967,37968,37969,37970,37971,37972,37973,37974,37975,37976,37977,37978,37979,37980,37981,37982,37983,37984,37985,37986,37987,37988,37989,37990,37991,37992,37993,37994,37996,37997,37998,37999,38000,38001,38002,38003,38004,38005,38006,38007,38008,38009,38010,38011,38012,38013,38014,38015,38016,38017,38018,38019,38020,38033,38038,38040,38087,38095,38099,38100,38106,38118,38139,38172,38176,29723,29722,29761,29788,29783,29781,29785,29815,29805,29822,29852,29838,29824,29825,29831,29835,29854,29864,29865,29840,29863,29906,29882,38890,38891,38892,26444,26451,26462,26440,26473,26533,26503,26474,26483,26520,26535,26485,26536,26526,26541,26507,26487,26492,26608,26633,26584,26634,26601,26544,26636,26585,26549,26586,26547,26589,26624,26563,26552,26594,26638,26561,26621,26674,26675,26720,26721,26702,26722,26692,26724,26755,26653,26709,26726,26689,26727,26688,26686,26698,26697,26665,26805,26767,26740,26743,26771,26731,26818,26990,26876,26911,26912,26873,38183,38195,38205,38211,38216,38219,38229,38234,38240,38254,38260,38261,38263,38264,38265,38266,38267,38268,38269,38270,38272,38273,38274,38275,38276,38277,38278,38279,38280,38281,38282,38283,38284,38285,38286,38287,38288,38289,38290,38291,38292,38293,38294,38295,38296,38297,38298,38299,38300,38301,38302,38303,38304,38305,38306,38307,38308,38309,38310,38311,38312,38313,38314,38315,38316,38317,38318,38319,38320,38321,38322,38323,38324,38325,38326,38327,38328,38329,38330,38331,38332,38333,38334,38335,38336,38337,38338,38339,38340,38341,38342,38343,38344,38345,38346,38347,26916,26864,26891,26881,26967,26851,26896,26993,26937,26976,26946,26973,27012,26987,27008,27032,27000,26932,27084,27015,27016,27086,27017,26982,26979,27001,27035,27047,27067,27051,27053,27092,27057,27073,27082,27103,27029,27104,27021,27135,27183,27117,27159,27160,27237,27122,27204,27198,27296,27216,27227,27189,27278,27257,27197,27176,27224,27260,27281,27280,27305,27287,27307,29495,29522,27521,27522,27527,27524,27538,27539,27533,27546,27547,27553,27562,36715,36717,36721,36722,36723,36725,36726,36728,36727,36729,36730,36732,36734,36737,36738,36740,36743,36747,38348,38349,38350,38351,38352,38353,38354,38355,38356,38357,38358,38359,38360,38361,38362,38363,38364,38365,38366,38367,38368,38369,38370,38371,38372,38373,38374,38375,38380,38399,38407,38419,38424,38427,38430,38432,38435,38436,38437,38438,38439,38440,38441,38443,38444,38445,38447,38448,38455,38456,38457,38458,38462,38465,38467,38474,38478,38479,38481,38482,38483,38486,38487,38488,38489,38490,38492,38493,38494,38496,38499,38501,38502,38507,38509,38510,38511,38512,38513,38515,38520,38521,38522,38523,38524,38525,38526,38527,38528,38529,38530,38531,38532,38535,38537,38538,36749,36750,36751,36760,36762,36558,25099,25111,25115,25119,25122,25121,25125,25124,25132,33255,29935,29940,29951,29967,29969,29971,25908,26094,26095,26096,26122,26137,26482,26115,26133,26112,28805,26359,26141,26164,26161,26166,26165,32774,26207,26196,26177,26191,26198,26209,26199,26231,26244,26252,26279,26269,26302,26331,26332,26342,26345,36146,36147,36150,36155,36157,36160,36165,36166,36168,36169,36167,36173,36181,36185,35271,35274,35275,35276,35278,35279,35280,35281,29294,29343,29277,29286,29295,29310,29311,29316,29323,29325,29327,29330,25352,25394,25520,38540,38542,38545,38546,38547,38549,38550,38554,38555,38557,38558,38559,38560,38561,38562,38563,38564,38565,38566,38568,38569,38570,38571,38572,38573,38574,38575,38577,38578,38580,38581,38583,38584,38586,38587,38591,38594,38595,38600,38602,38603,38608,38609,38611,38612,38614,38615,38616,38617,38618,38619,38620,38621,38622,38623,38625,38626,38627,38628,38629,38630,38631,38635,38636,38637,38638,38640,38641,38642,38644,38645,38648,38650,38651,38652,38653,38655,38658,38659,38661,38666,38667,38668,38672,38673,38674,38676,38677,38679,38680,38681,38682,38683,38685,38687,38688,25663,25816,32772,27626,27635,27645,27637,27641,27653,27655,27654,27661,27669,27672,27673,27674,27681,27689,27684,27690,27698,25909,25941,25963,29261,29266,29270,29232,34402,21014,32927,32924,32915,32956,26378,32957,32945,32939,32941,32948,32951,32999,33000,33001,33002,32987,32962,32964,32985,32973,32983,26384,32989,33003,33009,33012,33005,33037,33038,33010,33020,26389,33042,35930,33078,33054,33068,33048,33074,33096,33100,33107,33140,33113,33114,33137,33120,33129,33148,33149,33133,33127,22605,23221,33160,33154,33169,28373,33187,33194,33228,26406,33226,33211,38689,38690,38691,38692,38693,38694,38695,38696,38697,38699,38700,38702,38703,38705,38707,38708,38709,38710,38711,38714,38715,38716,38717,38719,38720,38721,38722,38723,38724,38725,38726,38727,38728,38729,38730,38731,38732,38733,38734,38735,38736,38737,38740,38741,38743,38744,38746,38748,38749,38751,38755,38756,38758,38759,38760,38762,38763,38764,38765,38766,38767,38768,38769,38770,38773,38775,38776,38777,38778,38779,38781,38782,38783,38784,38785,38786,38787,38788,38790,38791,38792,38793,38794,38796,38798,38799,38800,38803,38805,38806,38807,38809,38810,38811,38812,38813,33217,33190,27428,27447,27449,27459,27462,27481,39121,39122,39123,39125,39129,39130,27571,24384,27586,35315,26000,40785,26003,26044,26054,26052,26051,26060,26062,26066,26070,28800,28828,28822,28829,28859,28864,28855,28843,28849,28904,28874,28944,28947,28950,28975,28977,29043,29020,29032,28997,29042,29002,29048,29050,29080,29107,29109,29096,29088,29152,29140,29159,29177,29213,29224,28780,28952,29030,29113,25150,25149,25155,25160,25161,31035,31040,31046,31049,31067,31068,31059,31066,31074,31063,31072,31087,31079,31098,31109,31114,31130,31143,31155,24529,24528,38814,38815,38817,38818,38820,38821,38822,38823,38824,38825,38826,38828,38830,38832,38833,38835,38837,38838,38839,38840,38841,38842,38843,38844,38845,38846,38847,38848,38849,38850,38851,38852,38853,38854,38855,38856,38857,38858,38859,38860,38861,38862,38863,38864,38865,38866,38867,38868,38869,38870,38871,38872,38873,38874,38875,38876,38877,38878,38879,38880,38881,38882,38883,38884,38885,38888,38894,38895,38896,38897,38898,38900,38903,38904,38905,38906,38907,38908,38909,38910,38911,38912,38913,38914,38915,38916,38917,38918,38919,38920,38921,38922,38923,38924,38925,38926,24636,24669,24666,24679,24641,24665,24675,24747,24838,24845,24925,25001,24989,25035,25041,25094,32896,32895,27795,27894,28156,30710,30712,30720,30729,30743,30744,30737,26027,30765,30748,30749,30777,30778,30779,30751,30780,30757,30764,30755,30761,30798,30829,30806,30807,30758,30800,30791,30796,30826,30875,30867,30874,30855,30876,30881,30883,30898,30905,30885,30932,30937,30921,30956,30962,30981,30964,30995,31012,31006,31028,40859,40697,40699,40700,30449,30468,30477,30457,30471,30472,30490,30498,30489,30509,30502,30517,30520,30544,30545,30535,30531,30554,30568,38927,38928,38929,38930,38931,38932,38933,38934,38935,38936,38937,38938,38939,38940,38941,38942,38943,38944,38945,38946,38947,38948,38949,38950,38951,38952,38953,38954,38955,38956,38957,38958,38959,38960,38961,38962,38963,38964,38965,38966,38967,38968,38969,38970,38971,38972,38973,38974,38975,38976,38977,38978,38979,38980,38981,38982,38983,38984,38985,38986,38987,38988,38989,38990,38991,38992,38993,38994,38995,38996,38997,38998,38999,39000,39001,39002,39003,39004,39005,39006,39007,39008,39009,39010,39011,39012,39013,39014,39015,39016,39017,39018,39019,39020,39021,39022,30562,30565,30591,30605,30589,30592,30604,30609,30623,30624,30640,30645,30653,30010,30016,30030,30027,30024,30043,30066,30073,30083,32600,32609,32607,35400,32616,32628,32625,32633,32641,32638,30413,30437,34866,38021,38022,38023,38027,38026,38028,38029,38031,38032,38036,38039,38037,38042,38043,38044,38051,38052,38059,38058,38061,38060,38063,38064,38066,38068,38070,38071,38072,38073,38074,38076,38077,38079,38084,38088,38089,38090,38091,38092,38093,38094,38096,38097,38098,38101,38102,38103,38105,38104,38107,38110,38111,38112,38114,38116,38117,38119,38120,38122,39023,39024,39025,39026,39027,39028,39051,39054,39058,39061,39065,39075,39080,39081,39082,39083,39084,39085,39086,39087,39088,39089,39090,39091,39092,39093,39094,39095,39096,39097,39098,39099,39100,39101,39102,39103,39104,39105,39106,39107,39108,39109,39110,39111,39112,39113,39114,39115,39116,39117,39119,39120,39124,39126,39127,39131,39132,39133,39136,39137,39138,39139,39140,39141,39142,39145,39146,39147,39148,39149,39150,39151,39152,39153,39154,39155,39156,39157,39158,39159,39160,39161,39162,39163,39164,39165,39166,39167,39168,39169,39170,39171,39172,39173,39174,39175,38121,38123,38126,38127,38131,38132,38133,38135,38137,38140,38141,38143,38147,38146,38150,38151,38153,38154,38157,38158,38159,38162,38163,38164,38165,38166,38168,38171,38173,38174,38175,38178,38186,38187,38185,38188,38193,38194,38196,38198,38199,38200,38204,38206,38207,38210,38197,38212,38213,38214,38217,38220,38222,38223,38226,38227,38228,38230,38231,38232,38233,38235,38238,38239,38237,38241,38242,38244,38245,38246,38247,38248,38249,38250,38251,38252,38255,38257,38258,38259,38202,30695,30700,38601,31189,31213,31203,31211,31238,23879,31235,31234,31262,31252,39176,39177,39178,39179,39180,39182,39183,39185,39186,39187,39188,39189,39190,39191,39192,39193,39194,39195,39196,39197,39198,39199,39200,39201,39202,39203,39204,39205,39206,39207,39208,39209,39210,39211,39212,39213,39215,39216,39217,39218,39219,39220,39221,39222,39223,39224,39225,39226,39227,39228,39229,39230,39231,39232,39233,39234,39235,39236,39237,39238,39239,39240,39241,39242,39243,39244,39245,39246,39247,39248,39249,39250,39251,39254,39255,39256,39257,39258,39259,39260,39261,39262,39263,39264,39265,39266,39268,39270,39283,39288,39289,39291,39294,39298,39299,39305,31289,31287,31313,40655,39333,31344,30344,30350,30355,30361,30372,29918,29920,29996,40480,40482,40488,40489,40490,40491,40492,40498,40497,40502,40504,40503,40505,40506,40510,40513,40514,40516,40518,40519,40520,40521,40523,40524,40526,40529,40533,40535,40538,40539,40540,40542,40547,40550,40551,40552,40553,40554,40555,40556,40561,40557,40563,30098,30100,30102,30112,30109,30124,30115,30131,30132,30136,30148,30129,30128,30147,30146,30166,30157,30179,30184,30182,30180,30187,30183,30211,30193,30204,30207,30224,30208,30213,30220,30231,30218,30245,30232,30229,30233,39308,39310,39322,39323,39324,39325,39326,39327,39328,39329,39330,39331,39332,39334,39335,39337,39338,39339,39340,39341,39342,39343,39344,39345,39346,39347,39348,39349,39350,39351,39352,39353,39354,39355,39356,39357,39358,39359,39360,39361,39362,39363,39364,39365,39366,39367,39368,39369,39370,39371,39372,39373,39374,39375,39376,39377,39378,39379,39380,39381,39382,39383,39384,39385,39386,39387,39388,39389,39390,39391,39392,39393,39394,39395,39396,39397,39398,39399,39400,39401,39402,39403,39404,39405,39406,39407,39408,39409,39410,39411,39412,39413,39414,39415,39416,39417,30235,30268,30242,30240,30272,30253,30256,30271,30261,30275,30270,30259,30285,30302,30292,30300,30294,30315,30319,32714,31462,31352,31353,31360,31366,31368,31381,31398,31392,31404,31400,31405,31411,34916,34921,34930,34941,34943,34946,34978,35014,34999,35004,35017,35042,35022,35043,35045,35057,35098,35068,35048,35070,35056,35105,35097,35091,35099,35082,35124,35115,35126,35137,35174,35195,30091,32997,30386,30388,30684,32786,32788,32790,32796,32800,32802,32805,32806,32807,32809,32808,32817,32779,32821,32835,32838,32845,32850,32873,32881,35203,39032,39040,39043,39418,39419,39420,39421,39422,39423,39424,39425,39426,39427,39428,39429,39430,39431,39432,39433,39434,39435,39436,39437,39438,39439,39440,39441,39442,39443,39444,39445,39446,39447,39448,39449,39450,39451,39452,39453,39454,39455,39456,39457,39458,39459,39460,39461,39462,39463,39464,39465,39466,39467,39468,39469,39470,39471,39472,39473,39474,39475,39476,39477,39478,39479,39480,39481,39482,39483,39484,39485,39486,39487,39488,39489,39490,39491,39492,39493,39494,39495,39496,39497,39498,39499,39500,39501,39502,39503,39504,39505,39506,39507,39508,39509,39510,39511,39512,39513,39049,39052,39053,39055,39060,39066,39067,39070,39071,39073,39074,39077,39078,34381,34388,34412,34414,34431,34426,34428,34427,34472,34445,34443,34476,34461,34471,34467,34474,34451,34473,34486,34500,34485,34510,34480,34490,34481,34479,34505,34511,34484,34537,34545,34546,34541,34547,34512,34579,34526,34548,34527,34520,34513,34563,34567,34552,34568,34570,34573,34569,34595,34619,34590,34597,34606,34586,34622,34632,34612,34609,34601,34615,34623,34690,34594,34685,34686,34683,34656,34672,34636,34670,34699,34643,34659,34684,34660,34649,34661,34707,34735,34728,34770,39514,39515,39516,39517,39518,39519,39520,39521,39522,39523,39524,39525,39526,39527,39528,39529,39530,39531,39538,39555,39561,39565,39566,39572,39573,39577,39590,39593,39594,39595,39596,39597,39598,39599,39602,39603,39604,39605,39609,39611,39613,39614,39615,39619,39620,39622,39623,39624,39625,39626,39629,39630,39631,39632,39634,39636,39637,39638,39639,39641,39642,39643,39644,39645,39646,39648,39650,39651,39652,39653,39655,39656,39657,39658,39660,39662,39664,39665,39666,39667,39668,39669,39670,39671,39672,39674,39676,39677,39678,39679,39680,39681,39682,39684,39685,39686,34758,34696,34693,34733,34711,34691,34731,34789,34732,34741,34739,34763,34771,34749,34769,34752,34762,34779,34794,34784,34798,34838,34835,34814,34826,34843,34849,34873,34876,32566,32578,32580,32581,33296,31482,31485,31496,31491,31492,31509,31498,31531,31503,31559,31544,31530,31513,31534,31537,31520,31525,31524,31539,31550,31518,31576,31578,31557,31605,31564,31581,31584,31598,31611,31586,31602,31601,31632,31654,31655,31672,31660,31645,31656,31621,31658,31644,31650,31659,31668,31697,31681,31692,31709,31706,31717,31718,31722,31756,31742,31740,31759,31766,31755,39687,39689,39690,39691,39692,39693,39694,39696,39697,39698,39700,39701,39702,39703,39704,39705,39706,39707,39708,39709,39710,39712,39713,39714,39716,39717,39718,39719,39720,39721,39722,39723,39724,39725,39726,39728,39729,39731,39732,39733,39734,39735,39736,39737,39738,39741,39742,39743,39744,39750,39754,39755,39756,39758,39760,39762,39763,39765,39766,39767,39768,39769,39770,39771,39772,39773,39774,39775,39776,39777,39778,39779,39780,39781,39782,39783,39784,39785,39786,39787,39788,39789,39790,39791,39792,39793,39794,39795,39796,39797,39798,39799,39800,39801,39802,39803,31775,31786,31782,31800,31809,31808,33278,33281,33282,33284,33260,34884,33313,33314,33315,33325,33327,33320,33323,33336,33339,33331,33332,33342,33348,33353,33355,33359,33370,33375,33384,34942,34949,34952,35032,35039,35166,32669,32671,32679,32687,32688,32690,31868,25929,31889,31901,31900,31902,31906,31922,31932,31933,31937,31943,31948,31949,31944,31941,31959,31976,33390,26280,32703,32718,32725,32741,32737,32742,32745,32750,32755,31992,32119,32166,32174,32327,32411,40632,40628,36211,36228,36244,36241,36273,36199,36205,35911,35913,37194,37200,37198,37199,37220,39804,39805,39806,39807,39808,39809,39810,39811,39812,39813,39814,39815,39816,39817,39818,39819,39820,39821,39822,39823,39824,39825,39826,39827,39828,39829,39830,39831,39832,39833,39834,39835,39836,39837,39838,39839,39840,39841,39842,39843,39844,39845,39846,39847,39848,39849,39850,39851,39852,39853,39854,39855,39856,39857,39858,39859,39860,39861,39862,39863,39864,39865,39866,39867,39868,39869,39870,39871,39872,39873,39874,39875,39876,39877,39878,39879,39880,39881,39882,39883,39884,39885,39886,39887,39888,39889,39890,39891,39892,39893,39894,39895,39896,39897,39898,39899,37218,37217,37232,37225,37231,37245,37246,37234,37236,37241,37260,37253,37264,37261,37265,37282,37283,37290,37293,37294,37295,37301,37300,37306,35925,40574,36280,36331,36357,36441,36457,36277,36287,36284,36282,36292,36310,36311,36314,36318,36302,36303,36315,36294,36332,36343,36344,36323,36345,36347,36324,36361,36349,36372,36381,36383,36396,36398,36387,36399,36410,36416,36409,36405,36413,36401,36425,36417,36418,36433,36434,36426,36464,36470,36476,36463,36468,36485,36495,36500,36496,36508,36510,35960,35970,35978,35973,35992,35988,26011,35286,35294,35290,35292,39900,39901,39902,39903,39904,39905,39906,39907,39908,39909,39910,39911,39912,39913,39914,39915,39916,39917,39918,39919,39920,39921,39922,39923,39924,39925,39926,39927,39928,39929,39930,39931,39932,39933,39934,39935,39936,39937,39938,39939,39940,39941,39942,39943,39944,39945,39946,39947,39948,39949,39950,39951,39952,39953,39954,39955,39956,39957,39958,39959,39960,39961,39962,39963,39964,39965,39966,39967,39968,39969,39970,39971,39972,39973,39974,39975,39976,39977,39978,39979,39980,39981,39982,39983,39984,39985,39986,39987,39988,39989,39990,39991,39992,39993,39994,39995,35301,35307,35311,35390,35622,38739,38633,38643,38639,38662,38657,38664,38671,38670,38698,38701,38704,38718,40832,40835,40837,40838,40839,40840,40841,40842,40844,40702,40715,40717,38585,38588,38589,38606,38610,30655,38624,37518,37550,37576,37694,37738,37834,37775,37950,37995,40063,40066,40069,40070,40071,40072,31267,40075,40078,40080,40081,40082,40084,40085,40090,40091,40094,40095,40096,40097,40098,40099,40101,40102,40103,40104,40105,40107,40109,40110,40112,40113,40114,40115,40116,40117,40118,40119,40122,40123,40124,40125,40132,40133,40134,40135,40138,40139,39996,39997,39998,39999,40000,40001,40002,40003,40004,40005,40006,40007,40008,40009,40010,40011,40012,40013,40014,40015,40016,40017,40018,40019,40020,40021,40022,40023,40024,40025,40026,40027,40028,40029,40030,40031,40032,40033,40034,40035,40036,40037,40038,40039,40040,40041,40042,40043,40044,40045,40046,40047,40048,40049,40050,40051,40052,40053,40054,40055,40056,40057,40058,40059,40061,40062,40064,40067,40068,40073,40074,40076,40079,40083,40086,40087,40088,40089,40093,40106,40108,40111,40121,40126,40127,40128,40129,40130,40136,40137,40145,40146,40154,40155,40160,40161,40140,40141,40142,40143,40144,40147,40148,40149,40151,40152,40153,40156,40157,40159,40162,38780,38789,38801,38802,38804,38831,38827,38819,38834,38836,39601,39600,39607,40536,39606,39610,39612,39617,39616,39621,39618,39627,39628,39633,39749,39747,39751,39753,39752,39757,39761,39144,39181,39214,39253,39252,39647,39649,39654,39663,39659,39675,39661,39673,39688,39695,39699,39711,39715,40637,40638,32315,40578,40583,40584,40587,40594,37846,40605,40607,40667,40668,40669,40672,40671,40674,40681,40679,40677,40682,40687,40738,40748,40751,40761,40759,40765,40766,40772,40163,40164,40165,40166,40167,40168,40169,40170,40171,40172,40173,40174,40175,40176,40177,40178,40179,40180,40181,40182,40183,40184,40185,40186,40187,40188,40189,40190,40191,40192,40193,40194,40195,40196,40197,40198,40199,40200,40201,40202,40203,40204,40205,40206,40207,40208,40209,40210,40211,40212,40213,40214,40215,40216,40217,40218,40219,40220,40221,40222,40223,40224,40225,40226,40227,40228,40229,40230,40231,40232,40233,40234,40235,40236,40237,40238,40239,40240,40241,40242,40243,40244,40245,40246,40247,40248,40249,40250,40251,40252,40253,40254,40255,40256,40257,40258,57908,57909,57910,57911,57912,57913,57914,57915,57916,57917,57918,57919,57920,57921,57922,57923,57924,57925,57926,57927,57928,57929,57930,57931,57932,57933,57934,57935,57936,57937,57938,57939,57940,57941,57942,57943,57944,57945,57946,57947,57948,57949,57950,57951,57952,57953,57954,57955,57956,57957,57958,57959,57960,57961,57962,57963,57964,57965,57966,57967,57968,57969,57970,57971,57972,57973,57974,57975,57976,57977,57978,57979,57980,57981,57982,57983,57984,57985,57986,57987,57988,57989,57990,57991,57992,57993,57994,57995,57996,57997,57998,57999,58000,58001,40259,40260,40261,40262,40263,40264,40265,40266,40267,40268,40269,40270,40271,40272,40273,40274,40275,40276,40277,40278,40279,40280,40281,40282,40283,40284,40285,40286,40287,40288,40289,40290,40291,40292,40293,40294,40295,40296,40297,40298,40299,40300,40301,40302,40303,40304,40305,40306,40307,40308,40309,40310,40311,40312,40313,40314,40315,40316,40317,40318,40319,40320,40321,40322,40323,40324,40325,40326,40327,40328,40329,40330,40331,40332,40333,40334,40335,40336,40337,40338,40339,40340,40341,40342,40343,40344,40345,40346,40347,40348,40349,40350,40351,40352,40353,40354,58002,58003,58004,58005,58006,58007,58008,58009,58010,58011,58012,58013,58014,58015,58016,58017,58018,58019,58020,58021,58022,58023,58024,58025,58026,58027,58028,58029,58030,58031,58032,58033,58034,58035,58036,58037,58038,58039,58040,58041,58042,58043,58044,58045,58046,58047,58048,58049,58050,58051,58052,58053,58054,58055,58056,58057,58058,58059,58060,58061,58062,58063,58064,58065,58066,58067,58068,58069,58070,58071,58072,58073,58074,58075,58076,58077,58078,58079,58080,58081,58082,58083,58084,58085,58086,58087,58088,58089,58090,58091,58092,58093,58094,58095,40355,40356,40357,40358,40359,40360,40361,40362,40363,40364,40365,40366,40367,40368,40369,40370,40371,40372,40373,40374,40375,40376,40377,40378,40379,40380,40381,40382,40383,40384,40385,40386,40387,40388,40389,40390,40391,40392,40393,40394,40395,40396,40397,40398,40399,40400,40401,40402,40403,40404,40405,40406,40407,40408,40409,40410,40411,40412,40413,40414,40415,40416,40417,40418,40419,40420,40421,40422,40423,40424,40425,40426,40427,40428,40429,40430,40431,40432,40433,40434,40435,40436,40437,40438,40439,40440,40441,40442,40443,40444,40445,40446,40447,40448,40449,40450,58096,58097,58098,58099,58100,58101,58102,58103,58104,58105,58106,58107,58108,58109,58110,58111,58112,58113,58114,58115,58116,58117,58118,58119,58120,58121,58122,58123,58124,58125,58126,58127,58128,58129,58130,58131,58132,58133,58134,58135,58136,58137,58138,58139,58140,58141,58142,58143,58144,58145,58146,58147,58148,58149,58150,58151,58152,58153,58154,58155,58156,58157,58158,58159,58160,58161,58162,58163,58164,58165,58166,58167,58168,58169,58170,58171,58172,58173,58174,58175,58176,58177,58178,58179,58180,58181,58182,58183,58184,58185,58186,58187,58188,58189,40451,40452,40453,40454,40455,40456,40457,40458,40459,40460,40461,40462,40463,40464,40465,40466,40467,40468,40469,40470,40471,40472,40473,40474,40475,40476,40477,40478,40484,40487,40494,40496,40500,40507,40508,40512,40525,40528,40530,40531,40532,40534,40537,40541,40543,40544,40545,40546,40549,40558,40559,40562,40564,40565,40566,40567,40568,40569,40570,40571,40572,40573,40576,40577,40579,40580,40581,40582,40585,40586,40588,40589,40590,40591,40592,40593,40596,40597,40598,40599,40600,40601,40602,40603,40604,40606,40608,40609,40610,40611,40612,40613,40615,40616,40617,40618,58190,58191,58192,58193,58194,58195,58196,58197,58198,58199,58200,58201,58202,58203,58204,58205,58206,58207,58208,58209,58210,58211,58212,58213,58214,58215,58216,58217,58218,58219,58220,58221,58222,58223,58224,58225,58226,58227,58228,58229,58230,58231,58232,58233,58234,58235,58236,58237,58238,58239,58240,58241,58242,58243,58244,58245,58246,58247,58248,58249,58250,58251,58252,58253,58254,58255,58256,58257,58258,58259,58260,58261,58262,58263,58264,58265,58266,58267,58268,58269,58270,58271,58272,58273,58274,58275,58276,58277,58278,58279,58280,58281,58282,58283,40619,40620,40621,40622,40623,40624,40625,40626,40627,40629,40630,40631,40633,40634,40636,40639,40640,40641,40642,40643,40645,40646,40647,40648,40650,40651,40652,40656,40658,40659,40661,40662,40663,40665,40666,40670,40673,40675,40676,40678,40680,40683,40684,40685,40686,40688,40689,40690,40691,40692,40693,40694,40695,40696,40698,40701,40703,40704,40705,40706,40707,40708,40709,40710,40711,40712,40713,40714,40716,40719,40721,40722,40724,40725,40726,40728,40730,40731,40732,40733,40734,40735,40737,40739,40740,40741,40742,40743,40744,40745,40746,40747,40749,40750,40752,40753,58284,58285,58286,58287,58288,58289,58290,58291,58292,58293,58294,58295,58296,58297,58298,58299,58300,58301,58302,58303,58304,58305,58306,58307,58308,58309,58310,58311,58312,58313,58314,58315,58316,58317,58318,58319,58320,58321,58322,58323,58324,58325,58326,58327,58328,58329,58330,58331,58332,58333,58334,58335,58336,58337,58338,58339,58340,58341,58342,58343,58344,58345,58346,58347,58348,58349,58350,58351,58352,58353,58354,58355,58356,58357,58358,58359,58360,58361,58362,58363,58364,58365,58366,58367,58368,58369,58370,58371,58372,58373,58374,58375,58376,58377,40754,40755,40756,40757,40758,40760,40762,40764,40767,40768,40769,40770,40771,40773,40774,40775,40776,40777,40778,40779,40780,40781,40782,40783,40786,40787,40788,40789,40790,40791,40792,40793,40794,40795,40796,40797,40798,40799,40800,40801,40802,40803,40804,40805,40806,40807,40808,40809,40810,40811,40812,40813,40814,40815,40816,40817,40818,40819,40820,40821,40822,40823,40824,40825,40826,40827,40828,40829,40830,40833,40834,40845,40846,40847,40848,40849,40850,40851,40852,40853,40854,40855,40856,40860,40861,40862,40865,40866,40867,40868,40869,63788,63865,63893,63975,63985,58378,58379,58380,58381,58382,58383,58384,58385,58386,58387,58388,58389,58390,58391,58392,58393,58394,58395,58396,58397,58398,58399,58400,58401,58402,58403,58404,58405,58406,58407,58408,58409,58410,58411,58412,58413,58414,58415,58416,58417,58418,58419,58420,58421,58422,58423,58424,58425,58426,58427,58428,58429,58430,58431,58432,58433,58434,58435,58436,58437,58438,58439,58440,58441,58442,58443,58444,58445,58446,58447,58448,58449,58450,58451,58452,58453,58454,58455,58456,58457,58458,58459,58460,58461,58462,58463,58464,58465,58466,58467,58468,58469,58470,58471,64012,64013,64014,64015,64017,64019,64020,64024,64031,64032,64033,64035,64036,64039,64040,64041,11905,59414,59415,59416,11908,13427,13383,11912,11915,59422,13726,13850,13838,11916,11927,14702,14616,59430,14799,14815,14963,14800,59435,59436,15182,15470,15584,11943,59441,59442,11946,16470,16735,11950,17207,11955,11958,11959,59451,17329,17324,11963,17373,17622,18017,17996,59459,18211,18217,18300,18317,11978,18759,18810,18813,18818,18819,18821,18822,18847,18843,18871,18870,59476,59477,19619,19615,19616,19617,19575,19618,19731,19732,19733,19734,19735,19736,19737,19886,59492,58472,58473,58474,58475,58476,58477,58478,58479,58480,58481,58482,58483,58484,58485,58486,58487,58488,58489,58490,58491,58492,58493,58494,58495,58496,58497,58498,58499,58500,58501,58502,58503,58504,58505,58506,58507,58508,58509,58510,58511,58512,58513,58514,58515,58516,58517,58518,58519,58520,58521,58522,58523,58524,58525,58526,58527,58528,58529,58530,58531,58532,58533,58534,58535,58536,58537,58538,58539,58540,58541,58542,58543,58544,58545,58546,58547,58548,58549,58550,58551,58552,58553,58554,58555,58556,58557,58558,58559,58560,58561,58562,58563,58564,58565], + "gb18030-ranges":[[0,128],[36,165],[38,169],[45,178],[50,184],[81,216],[89,226],[95,235],[96,238],[100,244],[103,248],[104,251],[105,253],[109,258],[126,276],[133,284],[148,300],[172,325],[175,329],[179,334],[208,364],[306,463],[307,465],[308,467],[309,469],[310,471],[311,473],[312,475],[313,477],[341,506],[428,594],[443,610],[544,712],[545,716],[558,730],[741,930],[742,938],[749,962],[750,970],[805,1026],[819,1104],[820,1106],[7922,8209],[7924,8215],[7925,8218],[7927,8222],[7934,8231],[7943,8241],[7944,8244],[7945,8246],[7950,8252],[8062,8365],[8148,8452],[8149,8454],[8152,8458],[8164,8471],[8174,8482],[8236,8556],[8240,8570],[8262,8596],[8264,8602],[8374,8713],[8380,8720],[8381,8722],[8384,8726],[8388,8731],[8390,8737],[8392,8740],[8393,8742],[8394,8748],[8396,8751],[8401,8760],[8406,8766],[8416,8777],[8419,8781],[8424,8787],[8437,8802],[8439,8808],[8445,8816],[8482,8854],[8485,8858],[8496,8870],[8521,8896],[8603,8979],[8936,9322],[8946,9372],[9046,9548],[9050,9588],[9063,9616],[9066,9622],[9076,9634],[9092,9652],[9100,9662],[9108,9672],[9111,9676],[9113,9680],[9131,9702],[9162,9735],[9164,9738],[9218,9793],[9219,9795],[11329,11906],[11331,11909],[11334,11913],[11336,11917],[11346,11928],[11361,11944],[11363,11947],[11366,11951],[11370,11956],[11372,11960],[11375,11964],[11389,11979],[11682,12284],[11686,12292],[11687,12312],[11692,12319],[11694,12330],[11714,12351],[11716,12436],[11723,12447],[11725,12535],[11730,12543],[11736,12586],[11982,12842],[11989,12850],[12102,12964],[12336,13200],[12348,13215],[12350,13218],[12384,13253],[12393,13263],[12395,13267],[12397,13270],[12510,13384],[12553,13428],[12851,13727],[12962,13839],[12973,13851],[13738,14617],[13823,14703],[13919,14801],[13933,14816],[14080,14964],[14298,15183],[14585,15471],[14698,15585],[15583,16471],[15847,16736],[16318,17208],[16434,17325],[16438,17330],[16481,17374],[16729,17623],[17102,17997],[17122,18018],[17315,18212],[17320,18218],[17402,18301],[17418,18318],[17859,18760],[17909,18811],[17911,18814],[17915,18820],[17916,18823],[17936,18844],[17939,18848],[17961,18872],[18664,19576],[18703,19620],[18814,19738],[18962,19887],[19043,40870],[33469,59244],[33470,59336],[33471,59367],[33484,59413],[33485,59417],[33490,59423],[33497,59431],[33501,59437],[33505,59443],[33513,59452],[33520,59460],[33536,59478],[33550,59493],[37845,63789],[37921,63866],[37948,63894],[38029,63976],[38038,63986],[38064,64016],[38065,64018],[38066,64021],[38069,64025],[38075,64034],[38076,64037],[38078,64042],[39108,65074],[39109,65093],[39113,65107],[39114,65112],[39115,65127],[39116,65132],[39265,65375],[39394,65510],[189000,65536]], + "jis0208":[12288,12289,12290,65292,65294,12539,65306,65307,65311,65281,12443,12444,180,65344,168,65342,65507,65343,12541,12542,12445,12446,12291,20189,12293,12294,12295,12540,8213,8208,65295,65340,65374,8741,65372,8230,8229,8216,8217,8220,8221,65288,65289,12308,12309,65339,65341,65371,65373,12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,65291,65293,177,215,247,65309,8800,65308,65310,8806,8807,8734,8756,9794,9792,176,8242,8243,8451,65509,65284,65504,65505,65285,65283,65286,65290,65312,167,9734,9733,9675,9679,9678,9671,9670,9633,9632,9651,9650,9661,9660,8251,12306,8594,8592,8593,8595,12307,null,null,null,null,null,null,null,null,null,null,null,8712,8715,8838,8839,8834,8835,8746,8745,null,null,null,null,null,null,null,null,8743,8744,65506,8658,8660,8704,8707,null,null,null,null,null,null,null,null,null,null,null,8736,8869,8978,8706,8711,8801,8786,8810,8811,8730,8765,8733,8757,8747,8748,null,null,null,null,null,null,null,8491,8240,9839,9837,9834,8224,8225,182,null,null,null,null,9711,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,null,null,null,null,null,null,null,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,null,null,null,null,null,null,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,null,null,null,null,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,null,null,null,null,null,null,null,null,null,null,null,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,null,null,null,null,null,null,null,null,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,null,null,null,null,null,null,null,null,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,null,null,null,null,null,null,null,null,null,null,null,null,null,9472,9474,9484,9488,9496,9492,9500,9516,9508,9524,9532,9473,9475,9487,9491,9499,9495,9507,9523,9515,9531,9547,9504,9519,9512,9527,9535,9501,9520,9509,9528,9538,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9322,9323,9324,9325,9326,9327,9328,9329,9330,9331,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,null,13129,13076,13090,13133,13080,13095,13059,13110,13137,13143,13069,13094,13091,13099,13130,13115,13212,13213,13214,13198,13199,13252,13217,null,null,null,null,null,null,null,null,13179,12317,12319,8470,13261,8481,12964,12965,12966,12967,12968,12849,12850,12857,13182,13181,13180,8786,8801,8747,8750,8721,8730,8869,8736,8735,8895,8757,8745,8746,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20124,21782,23043,38463,21696,24859,25384,23030,36898,33909,33564,31312,24746,25569,28197,26093,33894,33446,39925,26771,22311,26017,25201,23451,22992,34427,39156,32098,32190,39822,25110,31903,34999,23433,24245,25353,26263,26696,38343,38797,26447,20197,20234,20301,20381,20553,22258,22839,22996,23041,23561,24799,24847,24944,26131,26885,28858,30031,30064,31227,32173,32239,32963,33806,34915,35586,36949,36986,21307,20117,20133,22495,32946,37057,30959,19968,22769,28322,36920,31282,33576,33419,39983,20801,21360,21693,21729,22240,23035,24341,39154,28139,32996,34093,38498,38512,38560,38907,21515,21491,23431,28879,32701,36802,38632,21359,40284,31418,19985,30867,33276,28198,22040,21764,27421,34074,39995,23013,21417,28006,29916,38287,22082,20113,36939,38642,33615,39180,21473,21942,23344,24433,26144,26355,26628,27704,27891,27945,29787,30408,31310,38964,33521,34907,35424,37613,28082,30123,30410,39365,24742,35585,36234,38322,27022,21421,20870,22290,22576,22852,23476,24310,24616,25513,25588,27839,28436,28814,28948,29017,29141,29503,32257,33398,33489,34199,36960,37467,40219,22633,26044,27738,29989,20985,22830,22885,24448,24540,25276,26106,27178,27431,27572,29579,32705,35158,40236,40206,40644,23713,27798,33659,20740,23627,25014,33222,26742,29281,20057,20474,21368,24681,28201,31311,38899,19979,21270,20206,20309,20285,20385,20339,21152,21487,22025,22799,23233,23478,23521,31185,26247,26524,26550,27468,27827,28779,29634,31117,31166,31292,31623,33457,33499,33540,33655,33775,33747,34662,35506,22057,36008,36838,36942,38686,34442,20420,23784,25105,29273,30011,33253,33469,34558,36032,38597,39187,39381,20171,20250,35299,22238,22602,22730,24315,24555,24618,24724,24674,25040,25106,25296,25913,39745,26214,26800,28023,28784,30028,30342,32117,33445,34809,38283,38542,35997,20977,21182,22806,21683,23475,23830,24936,27010,28079,30861,33995,34903,35442,37799,39608,28012,39336,34521,22435,26623,34510,37390,21123,22151,21508,24275,25313,25785,26684,26680,27579,29554,30906,31339,35226,35282,36203,36611,37101,38307,38548,38761,23398,23731,27005,38989,38990,25499,31520,27179,27263,26806,39949,28511,21106,21917,24688,25324,27963,28167,28369,33883,35088,36676,19988,39993,21494,26907,27194,38788,26666,20828,31427,33970,37340,37772,22107,40232,26658,33541,33841,31909,21000,33477,29926,20094,20355,20896,23506,21002,21208,21223,24059,21914,22570,23014,23436,23448,23515,24178,24185,24739,24863,24931,25022,25563,25954,26577,26707,26874,27454,27475,27735,28450,28567,28485,29872,29976,30435,30475,31487,31649,31777,32233,32566,32752,32925,33382,33694,35251,35532,36011,36996,37969,38291,38289,38306,38501,38867,39208,33304,20024,21547,23736,24012,29609,30284,30524,23721,32747,36107,38593,38929,38996,39000,20225,20238,21361,21916,22120,22522,22855,23305,23492,23696,24076,24190,24524,25582,26426,26071,26082,26399,26827,26820,27231,24112,27589,27671,27773,30079,31048,23395,31232,32000,24509,35215,35352,36020,36215,36556,36637,39138,39438,39740,20096,20605,20736,22931,23452,25135,25216,25836,27450,29344,30097,31047,32681,34811,35516,35696,25516,33738,38816,21513,21507,21931,26708,27224,35440,30759,26485,40653,21364,23458,33050,34384,36870,19992,20037,20167,20241,21450,21560,23470,24339,24613,25937,26429,27714,27762,27875,28792,29699,31350,31406,31496,32026,31998,32102,26087,29275,21435,23621,24040,25298,25312,25369,28192,34394,35377,36317,37624,28417,31142,39770,20136,20139,20140,20379,20384,20689,20807,31478,20849,20982,21332,21281,21375,21483,21932,22659,23777,24375,24394,24623,24656,24685,25375,25945,27211,27841,29378,29421,30703,33016,33029,33288,34126,37111,37857,38911,39255,39514,20208,20957,23597,26241,26989,23616,26354,26997,29577,26704,31873,20677,21220,22343,24062,37670,26020,27427,27453,29748,31105,31165,31563,32202,33465,33740,34943,35167,35641,36817,37329,21535,37504,20061,20534,21477,21306,29399,29590,30697,33510,36527,39366,39368,39378,20855,24858,34398,21936,31354,20598,23507,36935,38533,20018,27355,37351,23633,23624,25496,31391,27795,38772,36705,31402,29066,38536,31874,26647,32368,26705,37740,21234,21531,34219,35347,32676,36557,37089,21350,34952,31041,20418,20670,21009,20804,21843,22317,29674,22411,22865,24418,24452,24693,24950,24935,25001,25522,25658,25964,26223,26690,28179,30054,31293,31995,32076,32153,32331,32619,33550,33610,34509,35336,35427,35686,36605,38938,40335,33464,36814,39912,21127,25119,25731,28608,38553,26689,20625,27424,27770,28500,31348,32080,34880,35363,26376,20214,20537,20518,20581,20860,21048,21091,21927,22287,22533,23244,24314,25010,25080,25331,25458,26908,27177,29309,29356,29486,30740,30831,32121,30476,32937,35211,35609,36066,36562,36963,37749,38522,38997,39443,40568,20803,21407,21427,24187,24358,28187,28304,29572,29694,32067,33335,35328,35578,38480,20046,20491,21476,21628,22266,22993,23396,24049,24235,24359,25144,25925,26543,28246,29392,31946,34996,32929,32993,33776,34382,35463,36328,37431,38599,39015,40723,20116,20114,20237,21320,21577,21566,23087,24460,24481,24735,26791,27278,29786,30849,35486,35492,35703,37264,20062,39881,20132,20348,20399,20505,20502,20809,20844,21151,21177,21246,21402,21475,21521,21518,21897,22353,22434,22909,23380,23389,23439,24037,24039,24055,24184,24195,24218,24247,24344,24658,24908,25239,25304,25511,25915,26114,26179,26356,26477,26657,26775,27083,27743,27946,28009,28207,28317,30002,30343,30828,31295,31968,32005,32024,32094,32177,32789,32771,32943,32945,33108,33167,33322,33618,34892,34913,35611,36002,36092,37066,37237,37489,30783,37628,38308,38477,38917,39321,39640,40251,21083,21163,21495,21512,22741,25335,28640,35946,36703,40633,20811,21051,21578,22269,31296,37239,40288,40658,29508,28425,33136,29969,24573,24794,39592,29403,36796,27492,38915,20170,22256,22372,22718,23130,24680,25031,26127,26118,26681,26801,28151,30165,32058,33390,39746,20123,20304,21449,21766,23919,24038,24046,26619,27801,29811,30722,35408,37782,35039,22352,24231,25387,20661,20652,20877,26368,21705,22622,22971,23472,24425,25165,25505,26685,27507,28168,28797,37319,29312,30741,30758,31085,25998,32048,33756,35009,36617,38555,21092,22312,26448,32618,36001,20916,22338,38442,22586,27018,32948,21682,23822,22524,30869,40442,20316,21066,21643,25662,26152,26388,26613,31364,31574,32034,37679,26716,39853,31545,21273,20874,21047,23519,25334,25774,25830,26413,27578,34217,38609,30352,39894,25420,37638,39851,30399,26194,19977,20632,21442,23665,24808,25746,25955,26719,29158,29642,29987,31639,32386,34453,35715,36059,37240,39184,26028,26283,27531,20181,20180,20282,20351,21050,21496,21490,21987,22235,22763,22987,22985,23039,23376,23629,24066,24107,24535,24605,25351,25903,23388,26031,26045,26088,26525,27490,27515,27663,29509,31049,31169,31992,32025,32043,32930,33026,33267,35222,35422,35433,35430,35468,35566,36039,36060,38604,39164,27503,20107,20284,20365,20816,23383,23546,24904,25345,26178,27425,28363,27835,29246,29885,30164,30913,31034,32780,32819,33258,33940,36766,27728,40575,24335,35672,40235,31482,36600,23437,38635,19971,21489,22519,22833,23241,23460,24713,28287,28422,30142,36074,23455,34048,31712,20594,26612,33437,23649,34122,32286,33294,20889,23556,25448,36198,26012,29038,31038,32023,32773,35613,36554,36974,34503,37034,20511,21242,23610,26451,28796,29237,37196,37320,37675,33509,23490,24369,24825,20027,21462,23432,25163,26417,27530,29417,29664,31278,33131,36259,37202,39318,20754,21463,21610,23551,25480,27193,32172,38656,22234,21454,21608,23447,23601,24030,20462,24833,25342,27954,31168,31179,32066,32333,32722,33261,33311,33936,34886,35186,35728,36468,36655,36913,37195,37228,38598,37276,20160,20303,20805,21313,24467,25102,26580,27713,28171,29539,32294,37325,37507,21460,22809,23487,28113,31069,32302,31899,22654,29087,20986,34899,36848,20426,23803,26149,30636,31459,33308,39423,20934,24490,26092,26991,27529,28147,28310,28516,30462,32020,24033,36981,37255,38918,20966,21021,25152,26257,26329,28186,24246,32210,32626,26360,34223,34295,35576,21161,21465,22899,24207,24464,24661,37604,38500,20663,20767,21213,21280,21319,21484,21736,21830,21809,22039,22888,22974,23100,23477,23558,23567,23569,23578,24196,24202,24288,24432,25215,25220,25307,25484,25463,26119,26124,26157,26230,26494,26786,27167,27189,27836,28040,28169,28248,28988,28966,29031,30151,30465,30813,30977,31077,31216,31456,31505,31911,32057,32918,33750,33931,34121,34909,35059,35359,35388,35412,35443,35937,36062,37284,37478,37758,37912,38556,38808,19978,19976,19998,20055,20887,21104,22478,22580,22732,23330,24120,24773,25854,26465,26454,27972,29366,30067,31331,33976,35698,37304,37664,22065,22516,39166,25325,26893,27542,29165,32340,32887,33394,35302,39135,34645,36785,23611,20280,20449,20405,21767,23072,23517,23529,24515,24910,25391,26032,26187,26862,27035,28024,28145,30003,30137,30495,31070,31206,32051,33251,33455,34218,35242,35386,36523,36763,36914,37341,38663,20154,20161,20995,22645,22764,23563,29978,23613,33102,35338,36805,38499,38765,31525,35535,38920,37218,22259,21416,36887,21561,22402,24101,25512,27700,28810,30561,31883,32736,34928,36930,37204,37648,37656,38543,29790,39620,23815,23913,25968,26530,36264,38619,25454,26441,26905,33733,38935,38592,35070,28548,25722,23544,19990,28716,30045,26159,20932,21046,21218,22995,24449,24615,25104,25919,25972,26143,26228,26866,26646,27491,28165,29298,29983,30427,31934,32854,22768,35069,35199,35488,35475,35531,36893,37266,38738,38745,25993,31246,33030,38587,24109,24796,25114,26021,26132,26512,30707,31309,31821,32318,33034,36012,36196,36321,36447,30889,20999,25305,25509,25666,25240,35373,31363,31680,35500,38634,32118,33292,34633,20185,20808,21315,21344,23459,23554,23574,24029,25126,25159,25776,26643,26676,27849,27973,27927,26579,28508,29006,29053,26059,31359,31661,32218,32330,32680,33146,33307,33337,34214,35438,36046,36341,36984,36983,37549,37521,38275,39854,21069,21892,28472,28982,20840,31109,32341,33203,31950,22092,22609,23720,25514,26366,26365,26970,29401,30095,30094,30990,31062,31199,31895,32032,32068,34311,35380,38459,36961,40736,20711,21109,21452,21474,20489,21930,22766,22863,29245,23435,23652,21277,24803,24819,25436,25475,25407,25531,25805,26089,26361,24035,27085,27133,28437,29157,20105,30185,30456,31379,31967,32207,32156,32865,33609,33624,33900,33980,34299,35013,36208,36865,36973,37783,38684,39442,20687,22679,24974,33235,34101,36104,36896,20419,20596,21063,21363,24687,25417,26463,28204,36275,36895,20439,23646,36042,26063,32154,21330,34966,20854,25539,23384,23403,23562,25613,26449,36956,20182,22810,22826,27760,35409,21822,22549,22949,24816,25171,26561,33333,26965,38464,39364,39464,20307,22534,23550,32784,23729,24111,24453,24608,24907,25140,26367,27888,28382,32974,33151,33492,34955,36024,36864,36910,38538,40667,39899,20195,21488,22823,31532,37261,38988,40441,28381,28711,21331,21828,23429,25176,25246,25299,27810,28655,29730,35351,37944,28609,35582,33592,20967,34552,21482,21481,20294,36948,36784,22890,33073,24061,31466,36799,26842,35895,29432,40008,27197,35504,20025,21336,22022,22374,25285,25506,26086,27470,28129,28251,28845,30701,31471,31658,32187,32829,32966,34507,35477,37723,22243,22727,24382,26029,26262,27264,27573,30007,35527,20516,30693,22320,24347,24677,26234,27744,30196,31258,32622,33268,34584,36933,39347,31689,30044,31481,31569,33988,36880,31209,31378,33590,23265,30528,20013,20210,23449,24544,25277,26172,26609,27880,34411,34935,35387,37198,37619,39376,27159,28710,29482,33511,33879,36015,19969,20806,20939,21899,23541,24086,24115,24193,24340,24373,24427,24500,25074,25361,26274,26397,28526,29266,30010,30522,32884,33081,33144,34678,35519,35548,36229,36339,37530,38263,38914,40165,21189,25431,30452,26389,27784,29645,36035,37806,38515,27941,22684,26894,27084,36861,37786,30171,36890,22618,26626,25524,27131,20291,28460,26584,36795,34086,32180,37716,26943,28528,22378,22775,23340,32044,29226,21514,37347,40372,20141,20302,20572,20597,21059,35998,21576,22564,23450,24093,24213,24237,24311,24351,24716,25269,25402,25552,26799,27712,30855,31118,31243,32224,33351,35330,35558,36420,36883,37048,37165,37336,40718,27877,25688,25826,25973,28404,30340,31515,36969,37841,28346,21746,24505,25764,36685,36845,37444,20856,22635,22825,23637,24215,28155,32399,29980,36028,36578,39003,28857,20253,27583,28593,30000,38651,20814,21520,22581,22615,22956,23648,24466,26007,26460,28193,30331,33759,36077,36884,37117,37709,30757,30778,21162,24230,22303,22900,24594,20498,20826,20908,20941,20992,21776,22612,22616,22871,23445,23798,23947,24764,25237,25645,26481,26691,26812,26847,30423,28120,28271,28059,28783,29128,24403,30168,31095,31561,31572,31570,31958,32113,21040,33891,34153,34276,35342,35588,35910,36367,36867,36879,37913,38518,38957,39472,38360,20685,21205,21516,22530,23566,24999,25758,27934,30643,31461,33012,33796,36947,37509,23776,40199,21311,24471,24499,28060,29305,30563,31167,31716,27602,29420,35501,26627,27233,20984,31361,26932,23626,40182,33515,23493,37193,28702,22136,23663,24775,25958,27788,35930,36929,38931,21585,26311,37389,22856,37027,20869,20045,20970,34201,35598,28760,25466,37707,26978,39348,32260,30071,21335,26976,36575,38627,27741,20108,23612,24336,36841,21250,36049,32905,34425,24319,26085,20083,20837,22914,23615,38894,20219,22922,24525,35469,28641,31152,31074,23527,33905,29483,29105,24180,24565,25467,25754,29123,31896,20035,24316,20043,22492,22178,24745,28611,32013,33021,33075,33215,36786,35223,34468,24052,25226,25773,35207,26487,27874,27966,29750,30772,23110,32629,33453,39340,20467,24259,25309,25490,25943,26479,30403,29260,32972,32954,36649,37197,20493,22521,23186,26757,26995,29028,29437,36023,22770,36064,38506,36889,34687,31204,30695,33833,20271,21093,21338,25293,26575,27850,30333,31636,31893,33334,34180,36843,26333,28448,29190,32283,33707,39361,40614,20989,31665,30834,31672,32903,31560,27368,24161,32908,30033,30048,20843,37474,28300,30330,37271,39658,20240,32624,25244,31567,38309,40169,22138,22617,34532,38588,20276,21028,21322,21453,21467,24070,25644,26001,26495,27710,27726,29256,29359,29677,30036,32321,33324,34281,36009,31684,37318,29033,38930,39151,25405,26217,30058,30436,30928,34115,34542,21290,21329,21542,22915,24199,24444,24754,25161,25209,25259,26000,27604,27852,30130,30382,30865,31192,32203,32631,32933,34987,35513,36027,36991,38750,39131,27147,31800,20633,23614,24494,26503,27608,29749,30473,32654,40763,26570,31255,21305,30091,39661,24422,33181,33777,32920,24380,24517,30050,31558,36924,26727,23019,23195,32016,30334,35628,20469,24426,27161,27703,28418,29922,31080,34920,35413,35961,24287,25551,30149,31186,33495,37672,37618,33948,34541,39981,21697,24428,25996,27996,28693,36007,36051,38971,25935,29942,19981,20184,22496,22827,23142,23500,20904,24067,24220,24598,25206,25975,26023,26222,28014,29238,31526,33104,33178,33433,35676,36000,36070,36212,38428,38468,20398,25771,27494,33310,33889,34154,37096,23553,26963,39080,33914,34135,20239,21103,24489,24133,26381,31119,33145,35079,35206,28149,24343,25173,27832,20175,29289,39826,20998,21563,22132,22707,24996,25198,28954,22894,31881,31966,32027,38640,25991,32862,19993,20341,20853,22592,24163,24179,24330,26564,20006,34109,38281,38491,31859,38913,20731,22721,30294,30887,21029,30629,34065,31622,20559,22793,29255,31687,32232,36794,36820,36941,20415,21193,23081,24321,38829,20445,33303,37610,22275,25429,27497,29995,35036,36628,31298,21215,22675,24917,25098,26286,27597,31807,33769,20515,20472,21253,21574,22577,22857,23453,23792,23791,23849,24214,25265,25447,25918,26041,26379,27861,27873,28921,30770,32299,32990,33459,33804,34028,34562,35090,35370,35914,37030,37586,39165,40179,40300,20047,20129,20621,21078,22346,22952,24125,24536,24537,25151,26292,26395,26576,26834,20882,32033,32938,33192,35584,35980,36031,37502,38450,21536,38956,21271,20693,21340,22696,25778,26420,29287,30566,31302,37350,21187,27809,27526,22528,24140,22868,26412,32763,20961,30406,25705,30952,39764,40635,22475,22969,26151,26522,27598,21737,27097,24149,33180,26517,39850,26622,40018,26717,20134,20451,21448,25273,26411,27819,36804,20397,32365,40639,19975,24930,28288,28459,34067,21619,26410,39749,24051,31637,23724,23494,34588,28234,34001,31252,33032,22937,31885,27665,30496,21209,22818,28961,29279,30683,38695,40289,26891,23167,23064,20901,21517,21629,26126,30431,36855,37528,40180,23018,29277,28357,20813,26825,32191,32236,38754,40634,25720,27169,33538,22916,23391,27611,29467,30450,32178,32791,33945,20786,26408,40665,30446,26466,21247,39173,23588,25147,31870,36016,21839,24758,32011,38272,21249,20063,20918,22812,29242,32822,37326,24357,30690,21380,24441,32004,34220,35379,36493,38742,26611,34222,37971,24841,24840,27833,30290,35565,36664,21807,20305,20778,21191,21451,23461,24189,24736,24962,25558,26377,26586,28263,28044,29494,29495,30001,31056,35029,35480,36938,37009,37109,38596,34701,22805,20104,20313,19982,35465,36671,38928,20653,24188,22934,23481,24248,25562,25594,25793,26332,26954,27096,27915,28342,29076,29992,31407,32650,32768,33865,33993,35201,35617,36362,36965,38525,39178,24958,25233,27442,27779,28020,32716,32764,28096,32645,34746,35064,26469,33713,38972,38647,27931,32097,33853,37226,20081,21365,23888,27396,28651,34253,34349,35239,21033,21519,23653,26446,26792,29702,29827,30178,35023,35041,37324,38626,38520,24459,29575,31435,33870,25504,30053,21129,27969,28316,29705,30041,30827,31890,38534,31452,40845,20406,24942,26053,34396,20102,20142,20698,20001,20940,23534,26009,26753,28092,29471,30274,30637,31260,31975,33391,35538,36988,37327,38517,38936,21147,32209,20523,21400,26519,28107,29136,29747,33256,36650,38563,40023,40607,29792,22593,28057,32047,39006,20196,20278,20363,20919,21169,23994,24604,29618,31036,33491,37428,38583,38646,38666,40599,40802,26278,27508,21015,21155,28872,35010,24265,24651,24976,28451,29001,31806,32244,32879,34030,36899,37676,21570,39791,27347,28809,36034,36335,38706,21172,23105,24266,24324,26391,27004,27028,28010,28431,29282,29436,31725,32769,32894,34635,37070,20845,40595,31108,32907,37682,35542,20525,21644,35441,27498,36036,33031,24785,26528,40434,20121,20120,39952,35435,34241,34152,26880,28286,30871,33109,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,24332,19984,19989,20010,20017,20022,20028,20031,20034,20054,20056,20098,20101,35947,20106,33298,24333,20110,20126,20127,20128,20130,20144,20147,20150,20174,20173,20164,20166,20162,20183,20190,20205,20191,20215,20233,20314,20272,20315,20317,20311,20295,20342,20360,20367,20376,20347,20329,20336,20369,20335,20358,20374,20760,20436,20447,20430,20440,20443,20433,20442,20432,20452,20453,20506,20520,20500,20522,20517,20485,20252,20470,20513,20521,20524,20478,20463,20497,20486,20547,20551,26371,20565,20560,20552,20570,20566,20588,20600,20608,20634,20613,20660,20658,20681,20682,20659,20674,20694,20702,20709,20717,20707,20718,20729,20725,20745,20737,20738,20758,20757,20756,20762,20769,20794,20791,20796,20795,20799,20800,20818,20812,20820,20834,31480,20841,20842,20846,20864,20866,22232,20876,20873,20879,20881,20883,20885,20886,20900,20902,20898,20905,20906,20907,20915,20913,20914,20912,20917,20925,20933,20937,20955,20960,34389,20969,20973,20976,20981,20990,20996,21003,21012,21006,21031,21034,21038,21043,21049,21071,21060,21067,21068,21086,21076,21098,21108,21097,21107,21119,21117,21133,21140,21138,21105,21128,21137,36776,36775,21164,21165,21180,21173,21185,21197,21207,21214,21219,21222,39149,21216,21235,21237,21240,21241,21254,21256,30008,21261,21264,21263,21269,21274,21283,21295,21297,21299,21304,21312,21318,21317,19991,21321,21325,20950,21342,21353,21358,22808,21371,21367,21378,21398,21408,21414,21413,21422,21424,21430,21443,31762,38617,21471,26364,29166,21486,21480,21485,21498,21505,21565,21568,21548,21549,21564,21550,21558,21545,21533,21582,21647,21621,21646,21599,21617,21623,21616,21650,21627,21632,21622,21636,21648,21638,21703,21666,21688,21669,21676,21700,21704,21672,21675,21698,21668,21694,21692,21720,21733,21734,21775,21780,21757,21742,21741,21754,21730,21817,21824,21859,21836,21806,21852,21829,21846,21847,21816,21811,21853,21913,21888,21679,21898,21919,21883,21886,21912,21918,21934,21884,21891,21929,21895,21928,21978,21957,21983,21956,21980,21988,21972,22036,22007,22038,22014,22013,22043,22009,22094,22096,29151,22068,22070,22066,22072,22123,22116,22063,22124,22122,22150,22144,22154,22176,22164,22159,22181,22190,22198,22196,22210,22204,22209,22211,22208,22216,22222,22225,22227,22231,22254,22265,22272,22271,22276,22281,22280,22283,22285,22291,22296,22294,21959,22300,22310,22327,22328,22350,22331,22336,22351,22377,22464,22408,22369,22399,22409,22419,22432,22451,22436,22442,22448,22467,22470,22484,22482,22483,22538,22486,22499,22539,22553,22557,22642,22561,22626,22603,22640,27584,22610,22589,22649,22661,22713,22687,22699,22714,22750,22715,22712,22702,22725,22739,22737,22743,22745,22744,22757,22748,22756,22751,22767,22778,22777,22779,22780,22781,22786,22794,22800,22811,26790,22821,22828,22829,22834,22840,22846,31442,22869,22864,22862,22874,22872,22882,22880,22887,22892,22889,22904,22913,22941,20318,20395,22947,22962,22982,23016,23004,22925,23001,23002,23077,23071,23057,23068,23049,23066,23104,23148,23113,23093,23094,23138,23146,23194,23228,23230,23243,23234,23229,23267,23255,23270,23273,23254,23290,23291,23308,23307,23318,23346,23248,23338,23350,23358,23363,23365,23360,23377,23381,23386,23387,23397,23401,23408,23411,23413,23416,25992,23418,23424,23427,23462,23480,23491,23495,23497,23508,23504,23524,23526,23522,23518,23525,23531,23536,23542,23539,23557,23559,23560,23565,23571,23584,23586,23592,23608,23609,23617,23622,23630,23635,23632,23631,23409,23660,23662,20066,23670,23673,23692,23697,23700,22939,23723,23739,23734,23740,23735,23749,23742,23751,23769,23785,23805,23802,23789,23948,23786,23819,23829,23831,23900,23839,23835,23825,23828,23842,23834,23833,23832,23884,23890,23886,23883,23916,23923,23926,23943,23940,23938,23970,23965,23980,23982,23997,23952,23991,23996,24009,24013,24019,24018,24022,24027,24043,24050,24053,24075,24090,24089,24081,24091,24118,24119,24132,24131,24128,24142,24151,24148,24159,24162,24164,24135,24181,24182,24186,40636,24191,24224,24257,24258,24264,24272,24271,24278,24291,24285,24282,24283,24290,24289,24296,24297,24300,24305,24307,24304,24308,24312,24318,24323,24329,24413,24412,24331,24337,24342,24361,24365,24376,24385,24392,24396,24398,24367,24401,24406,24407,24409,24417,24429,24435,24439,24451,24450,24447,24458,24456,24465,24455,24478,24473,24472,24480,24488,24493,24508,24534,24571,24548,24568,24561,24541,24755,24575,24609,24672,24601,24592,24617,24590,24625,24603,24597,24619,24614,24591,24634,24666,24641,24682,24695,24671,24650,24646,24653,24675,24643,24676,24642,24684,24683,24665,24705,24717,24807,24707,24730,24708,24731,24726,24727,24722,24743,24715,24801,24760,24800,24787,24756,24560,24765,24774,24757,24792,24909,24853,24838,24822,24823,24832,24820,24826,24835,24865,24827,24817,24845,24846,24903,24894,24872,24871,24906,24895,24892,24876,24884,24893,24898,24900,24947,24951,24920,24921,24922,24939,24948,24943,24933,24945,24927,24925,24915,24949,24985,24982,24967,25004,24980,24986,24970,24977,25003,25006,25036,25034,25033,25079,25032,25027,25030,25018,25035,32633,25037,25062,25059,25078,25082,25076,25087,25085,25084,25086,25088,25096,25097,25101,25100,25108,25115,25118,25121,25130,25134,25136,25138,25139,25153,25166,25182,25187,25179,25184,25192,25212,25218,25225,25214,25234,25235,25238,25300,25219,25236,25303,25297,25275,25295,25343,25286,25812,25288,25308,25292,25290,25282,25287,25243,25289,25356,25326,25329,25383,25346,25352,25327,25333,25424,25406,25421,25628,25423,25494,25486,25472,25515,25462,25507,25487,25481,25503,25525,25451,25449,25534,25577,25536,25542,25571,25545,25554,25590,25540,25622,25652,25606,25619,25638,25654,25885,25623,25640,25615,25703,25711,25718,25678,25898,25749,25747,25765,25769,25736,25788,25818,25810,25797,25799,25787,25816,25794,25841,25831,33289,25824,25825,25260,25827,25839,25900,25846,25844,25842,25850,25856,25853,25880,25884,25861,25892,25891,25899,25908,25909,25911,25910,25912,30027,25928,25942,25941,25933,25944,25950,25949,25970,25976,25986,25987,35722,26011,26015,26027,26039,26051,26054,26049,26052,26060,26066,26075,26073,26080,26081,26097,26482,26122,26115,26107,26483,26165,26166,26164,26140,26191,26180,26185,26177,26206,26205,26212,26215,26216,26207,26210,26224,26243,26248,26254,26249,26244,26264,26269,26305,26297,26313,26302,26300,26308,26296,26326,26330,26336,26175,26342,26345,26352,26357,26359,26383,26390,26398,26406,26407,38712,26414,26431,26422,26433,26424,26423,26438,26462,26464,26457,26467,26468,26505,26480,26537,26492,26474,26508,26507,26534,26529,26501,26551,26607,26548,26604,26547,26601,26552,26596,26590,26589,26594,26606,26553,26574,26566,26599,27292,26654,26694,26665,26688,26701,26674,26702,26803,26667,26713,26723,26743,26751,26783,26767,26797,26772,26781,26779,26755,27310,26809,26740,26805,26784,26810,26895,26765,26750,26881,26826,26888,26840,26914,26918,26849,26892,26829,26836,26855,26837,26934,26898,26884,26839,26851,26917,26873,26848,26863,26920,26922,26906,26915,26913,26822,27001,26999,26972,27000,26987,26964,27006,26990,26937,26996,26941,26969,26928,26977,26974,26973,27009,26986,27058,27054,27088,27071,27073,27091,27070,27086,23528,27082,27101,27067,27075,27047,27182,27025,27040,27036,27029,27060,27102,27112,27138,27163,27135,27402,27129,27122,27111,27141,27057,27166,27117,27156,27115,27146,27154,27329,27171,27155,27204,27148,27250,27190,27256,27207,27234,27225,27238,27208,27192,27170,27280,27277,27296,27268,27298,27299,27287,34327,27323,27331,27330,27320,27315,27308,27358,27345,27359,27306,27354,27370,27387,27397,34326,27386,27410,27414,39729,27423,27448,27447,30428,27449,39150,27463,27459,27465,27472,27481,27476,27483,27487,27489,27512,27513,27519,27520,27524,27523,27533,27544,27541,27550,27556,27562,27563,27567,27570,27569,27571,27575,27580,27590,27595,27603,27615,27628,27627,27635,27631,40638,27656,27667,27668,27675,27684,27683,27742,27733,27746,27754,27778,27789,27802,27777,27803,27774,27752,27763,27794,27792,27844,27889,27859,27837,27863,27845,27869,27822,27825,27838,27834,27867,27887,27865,27882,27935,34893,27958,27947,27965,27960,27929,27957,27955,27922,27916,28003,28051,28004,27994,28025,27993,28046,28053,28644,28037,28153,28181,28170,28085,28103,28134,28088,28102,28140,28126,28108,28136,28114,28101,28154,28121,28132,28117,28138,28142,28205,28270,28206,28185,28274,28255,28222,28195,28267,28203,28278,28237,28191,28227,28218,28238,28196,28415,28189,28216,28290,28330,28312,28361,28343,28371,28349,28335,28356,28338,28372,28373,28303,28325,28354,28319,28481,28433,28748,28396,28408,28414,28479,28402,28465,28399,28466,28364,28478,28435,28407,28550,28538,28536,28545,28544,28527,28507,28659,28525,28546,28540,28504,28558,28561,28610,28518,28595,28579,28577,28580,28601,28614,28586,28639,28629,28652,28628,28632,28657,28654,28635,28681,28683,28666,28689,28673,28687,28670,28699,28698,28532,28701,28696,28703,28720,28734,28722,28753,28771,28825,28818,28847,28913,28844,28856,28851,28846,28895,28875,28893,28889,28937,28925,28956,28953,29029,29013,29064,29030,29026,29004,29014,29036,29071,29179,29060,29077,29096,29100,29143,29113,29118,29138,29129,29140,29134,29152,29164,29159,29173,29180,29177,29183,29197,29200,29211,29224,29229,29228,29232,29234,29243,29244,29247,29248,29254,29259,29272,29300,29310,29314,29313,29319,29330,29334,29346,29351,29369,29362,29379,29382,29380,29390,29394,29410,29408,29409,29433,29431,20495,29463,29450,29468,29462,29469,29492,29487,29481,29477,29502,29518,29519,40664,29527,29546,29544,29552,29560,29557,29563,29562,29640,29619,29646,29627,29632,29669,29678,29662,29858,29701,29807,29733,29688,29746,29754,29781,29759,29791,29785,29761,29788,29801,29808,29795,29802,29814,29822,29835,29854,29863,29898,29903,29908,29681,29920,29923,29927,29929,29934,29938,29936,29937,29944,29943,29956,29955,29957,29964,29966,29965,29973,29971,29982,29990,29996,30012,30020,30029,30026,30025,30043,30022,30042,30057,30052,30055,30059,30061,30072,30070,30086,30087,30068,30090,30089,30082,30100,30106,30109,30117,30115,30146,30131,30147,30133,30141,30136,30140,30129,30157,30154,30162,30169,30179,30174,30206,30207,30204,30209,30192,30202,30194,30195,30219,30221,30217,30239,30247,30240,30241,30242,30244,30260,30256,30267,30279,30280,30278,30300,30296,30305,30306,30312,30313,30314,30311,30316,30320,30322,30326,30328,30332,30336,30339,30344,30347,30350,30358,30355,30361,30362,30384,30388,30392,30393,30394,30402,30413,30422,30418,30430,30433,30437,30439,30442,34351,30459,30472,30471,30468,30505,30500,30494,30501,30502,30491,30519,30520,30535,30554,30568,30571,30555,30565,30591,30590,30585,30606,30603,30609,30624,30622,30640,30646,30649,30655,30652,30653,30651,30663,30669,30679,30682,30684,30691,30702,30716,30732,30738,31014,30752,31018,30789,30862,30836,30854,30844,30874,30860,30883,30901,30890,30895,30929,30918,30923,30932,30910,30908,30917,30922,30956,30951,30938,30973,30964,30983,30994,30993,31001,31020,31019,31040,31072,31063,31071,31066,31061,31059,31098,31103,31114,31133,31143,40779,31146,31150,31155,31161,31162,31177,31189,31207,31212,31201,31203,31240,31245,31256,31257,31264,31263,31104,31281,31291,31294,31287,31299,31319,31305,31329,31330,31337,40861,31344,31353,31357,31368,31383,31381,31384,31382,31401,31432,31408,31414,31429,31428,31423,36995,31431,31434,31437,31439,31445,31443,31449,31450,31453,31457,31458,31462,31469,31472,31490,31503,31498,31494,31539,31512,31513,31518,31541,31528,31542,31568,31610,31492,31565,31499,31564,31557,31605,31589,31604,31591,31600,31601,31596,31598,31645,31640,31647,31629,31644,31642,31627,31634,31631,31581,31641,31691,31681,31692,31695,31668,31686,31709,31721,31761,31764,31718,31717,31840,31744,31751,31763,31731,31735,31767,31757,31734,31779,31783,31786,31775,31799,31787,31805,31820,31811,31828,31823,31808,31824,31832,31839,31844,31830,31845,31852,31861,31875,31888,31908,31917,31906,31915,31905,31912,31923,31922,31921,31918,31929,31933,31936,31941,31938,31960,31954,31964,31970,39739,31983,31986,31988,31990,31994,32006,32002,32028,32021,32010,32069,32075,32046,32050,32063,32053,32070,32115,32086,32078,32114,32104,32110,32079,32099,32147,32137,32091,32143,32125,32155,32186,32174,32163,32181,32199,32189,32171,32317,32162,32175,32220,32184,32159,32176,32216,32221,32228,32222,32251,32242,32225,32261,32266,32291,32289,32274,32305,32287,32265,32267,32290,32326,32358,32315,32309,32313,32323,32311,32306,32314,32359,32349,32342,32350,32345,32346,32377,32362,32361,32380,32379,32387,32213,32381,36782,32383,32392,32393,32396,32402,32400,32403,32404,32406,32398,32411,32412,32568,32570,32581,32588,32589,32590,32592,32593,32597,32596,32600,32607,32608,32616,32617,32615,32632,32642,32646,32643,32648,32647,32652,32660,32670,32669,32666,32675,32687,32690,32697,32686,32694,32696,35697,32709,32710,32714,32725,32724,32737,32742,32745,32755,32761,39132,32774,32772,32779,32786,32792,32793,32796,32801,32808,32831,32827,32842,32838,32850,32856,32858,32863,32866,32872,32883,32882,32880,32886,32889,32893,32895,32900,32902,32901,32923,32915,32922,32941,20880,32940,32987,32997,32985,32989,32964,32986,32982,33033,33007,33009,33051,33065,33059,33071,33099,38539,33094,33086,33107,33105,33020,33137,33134,33125,33126,33140,33155,33160,33162,33152,33154,33184,33173,33188,33187,33119,33171,33193,33200,33205,33214,33208,33213,33216,33218,33210,33225,33229,33233,33241,33240,33224,33242,33247,33248,33255,33274,33275,33278,33281,33282,33285,33287,33290,33293,33296,33302,33321,33323,33336,33331,33344,33369,33368,33373,33370,33375,33380,33378,33384,33386,33387,33326,33393,33399,33400,33406,33421,33426,33451,33439,33467,33452,33505,33507,33503,33490,33524,33523,33530,33683,33539,33531,33529,33502,33542,33500,33545,33497,33589,33588,33558,33586,33585,33600,33593,33616,33605,33583,33579,33559,33560,33669,33690,33706,33695,33698,33686,33571,33678,33671,33674,33660,33717,33651,33653,33696,33673,33704,33780,33811,33771,33742,33789,33795,33752,33803,33729,33783,33799,33760,33778,33805,33826,33824,33725,33848,34054,33787,33901,33834,33852,34138,33924,33911,33899,33965,33902,33922,33897,33862,33836,33903,33913,33845,33994,33890,33977,33983,33951,34009,33997,33979,34010,34000,33985,33990,34006,33953,34081,34047,34036,34071,34072,34092,34079,34069,34068,34044,34112,34147,34136,34120,34113,34306,34123,34133,34176,34212,34184,34193,34186,34216,34157,34196,34203,34282,34183,34204,34167,34174,34192,34249,34234,34255,34233,34256,34261,34269,34277,34268,34297,34314,34323,34315,34302,34298,34310,34338,34330,34352,34367,34381,20053,34388,34399,34407,34417,34451,34467,34473,34474,34443,34444,34486,34479,34500,34502,34480,34505,34851,34475,34516,34526,34537,34540,34527,34523,34543,34578,34566,34568,34560,34563,34555,34577,34569,34573,34553,34570,34612,34623,34615,34619,34597,34601,34586,34656,34655,34680,34636,34638,34676,34647,34664,34670,34649,34643,34659,34666,34821,34722,34719,34690,34735,34763,34749,34752,34768,38614,34731,34756,34739,34759,34758,34747,34799,34802,34784,34831,34829,34814,34806,34807,34830,34770,34833,34838,34837,34850,34849,34865,34870,34873,34855,34875,34884,34882,34898,34905,34910,34914,34923,34945,34942,34974,34933,34941,34997,34930,34946,34967,34962,34990,34969,34978,34957,34980,34992,35007,34993,35011,35012,35028,35032,35033,35037,35065,35074,35068,35060,35048,35058,35076,35084,35082,35091,35139,35102,35109,35114,35115,35137,35140,35131,35126,35128,35148,35101,35168,35166,35174,35172,35181,35178,35183,35188,35191,35198,35203,35208,35210,35219,35224,35233,35241,35238,35244,35247,35250,35258,35261,35263,35264,35290,35292,35293,35303,35316,35320,35331,35350,35344,35340,35355,35357,35365,35382,35393,35419,35410,35398,35400,35452,35437,35436,35426,35461,35458,35460,35496,35489,35473,35493,35494,35482,35491,35524,35533,35522,35546,35563,35571,35559,35556,35569,35604,35552,35554,35575,35550,35547,35596,35591,35610,35553,35606,35600,35607,35616,35635,38827,35622,35627,35646,35624,35649,35660,35663,35662,35657,35670,35675,35674,35691,35679,35692,35695,35700,35709,35712,35724,35726,35730,35731,35734,35737,35738,35898,35905,35903,35912,35916,35918,35920,35925,35938,35948,35960,35962,35970,35977,35973,35978,35981,35982,35988,35964,35992,25117,36013,36010,36029,36018,36019,36014,36022,36040,36033,36068,36067,36058,36093,36090,36091,36100,36101,36106,36103,36111,36109,36112,40782,36115,36045,36116,36118,36199,36205,36209,36211,36225,36249,36290,36286,36282,36303,36314,36310,36300,36315,36299,36330,36331,36319,36323,36348,36360,36361,36351,36381,36382,36368,36383,36418,36405,36400,36404,36426,36423,36425,36428,36432,36424,36441,36452,36448,36394,36451,36437,36470,36466,36476,36481,36487,36485,36484,36491,36490,36499,36497,36500,36505,36522,36513,36524,36528,36550,36529,36542,36549,36552,36555,36571,36579,36604,36603,36587,36606,36618,36613,36629,36626,36633,36627,36636,36639,36635,36620,36646,36659,36667,36665,36677,36674,36670,36684,36681,36678,36686,36695,36700,36706,36707,36708,36764,36767,36771,36781,36783,36791,36826,36837,36834,36842,36847,36999,36852,36869,36857,36858,36881,36885,36897,36877,36894,36886,36875,36903,36918,36917,36921,36856,36943,36944,36945,36946,36878,36937,36926,36950,36952,36958,36968,36975,36982,38568,36978,36994,36989,36993,36992,37002,37001,37007,37032,37039,37041,37045,37090,37092,25160,37083,37122,37138,37145,37170,37168,37194,37206,37208,37219,37221,37225,37235,37234,37259,37257,37250,37282,37291,37295,37290,37301,37300,37306,37312,37313,37321,37323,37328,37334,37343,37345,37339,37372,37365,37366,37406,37375,37396,37420,37397,37393,37470,37463,37445,37449,37476,37448,37525,37439,37451,37456,37532,37526,37523,37531,37466,37583,37561,37559,37609,37647,37626,37700,37678,37657,37666,37658,37667,37690,37685,37691,37724,37728,37756,37742,37718,37808,37804,37805,37780,37817,37846,37847,37864,37861,37848,37827,37853,37840,37832,37860,37914,37908,37907,37891,37895,37904,37942,37931,37941,37921,37946,37953,37970,37956,37979,37984,37986,37982,37994,37417,38000,38005,38007,38013,37978,38012,38014,38017,38015,38274,38279,38282,38292,38294,38296,38297,38304,38312,38311,38317,38332,38331,38329,38334,38346,28662,38339,38349,38348,38357,38356,38358,38364,38369,38373,38370,38433,38440,38446,38447,38466,38476,38479,38475,38519,38492,38494,38493,38495,38502,38514,38508,38541,38552,38549,38551,38570,38567,38577,38578,38576,38580,38582,38584,38585,38606,38603,38601,38605,35149,38620,38669,38613,38649,38660,38662,38664,38675,38670,38673,38671,38678,38681,38692,38698,38704,38713,38717,38718,38724,38726,38728,38722,38729,38748,38752,38756,38758,38760,21202,38763,38769,38777,38789,38780,38785,38778,38790,38795,38799,38800,38812,38824,38822,38819,38835,38836,38851,38854,38856,38859,38876,38893,40783,38898,31455,38902,38901,38927,38924,38968,38948,38945,38967,38973,38982,38991,38987,39019,39023,39024,39025,39028,39027,39082,39087,39089,39094,39108,39107,39110,39145,39147,39171,39177,39186,39188,39192,39201,39197,39198,39204,39200,39212,39214,39229,39230,39234,39241,39237,39248,39243,39249,39250,39244,39253,39319,39320,39333,39341,39342,39356,39391,39387,39389,39384,39377,39405,39406,39409,39410,39419,39416,39425,39439,39429,39394,39449,39467,39479,39493,39490,39488,39491,39486,39509,39501,39515,39511,39519,39522,39525,39524,39529,39531,39530,39597,39600,39612,39616,39631,39633,39635,39636,39646,39647,39650,39651,39654,39663,39659,39662,39668,39665,39671,39675,39686,39704,39706,39711,39714,39715,39717,39719,39720,39721,39722,39726,39727,39730,39748,39747,39759,39757,39758,39761,39768,39796,39827,39811,39825,39830,39831,39839,39840,39848,39860,39872,39882,39865,39878,39887,39889,39890,39907,39906,39908,39892,39905,39994,39922,39921,39920,39957,39956,39945,39955,39948,39942,39944,39954,39946,39940,39982,39963,39973,39972,39969,39984,40007,39986,40006,39998,40026,40032,40039,40054,40056,40167,40172,40176,40201,40200,40171,40195,40198,40234,40230,40367,40227,40223,40260,40213,40210,40257,40255,40254,40262,40264,40285,40286,40292,40273,40272,40281,40306,40329,40327,40363,40303,40314,40346,40356,40361,40370,40388,40385,40379,40376,40378,40390,40399,40386,40409,40403,40440,40422,40429,40431,40445,40474,40475,40478,40565,40569,40573,40577,40584,40587,40588,40594,40597,40593,40605,40613,40617,40632,40618,40621,38753,40652,40654,40655,40656,40660,40668,40670,40669,40672,40677,40680,40687,40692,40694,40695,40697,40699,40700,40701,40711,40712,30391,40725,40737,40748,40766,40778,40786,40788,40803,40799,40800,40801,40806,40807,40812,40810,40823,40818,40822,40853,40860,40864,22575,27079,36953,29796,20956,29081,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32394,35100,37704,37512,34012,20425,28859,26161,26824,37625,26363,24389,20008,20193,20220,20224,20227,20281,20310,20370,20362,20378,20372,20429,20544,20514,20479,20510,20550,20592,20546,20628,20724,20696,20810,20836,20893,20926,20972,21013,21148,21158,21184,21211,21248,21255,21284,21362,21395,21426,21469,64014,21660,21642,21673,21759,21894,22361,22373,22444,22472,22471,64015,64016,22686,22706,22795,22867,22875,22877,22883,22948,22970,23382,23488,29999,23512,23532,23582,23718,23738,23797,23847,23891,64017,23874,23917,23992,23993,24016,24353,24372,24423,24503,24542,24669,24709,24714,24798,24789,24864,24818,24849,24887,24880,24984,25107,25254,25589,25696,25757,25806,25934,26112,26133,26171,26121,26158,26142,26148,26213,26199,26201,64018,26227,26265,26272,26290,26303,26362,26382,63785,26470,26555,26706,26560,26625,26692,26831,64019,26984,64020,27032,27106,27184,27243,27206,27251,27262,27362,27364,27606,27711,27740,27782,27759,27866,27908,28039,28015,28054,28076,28111,28152,28146,28156,28217,28252,28199,28220,28351,28552,28597,28661,28677,28679,28712,28805,28843,28943,28932,29020,28998,28999,64021,29121,29182,29361,29374,29476,64022,29559,29629,29641,29654,29667,29650,29703,29685,29734,29738,29737,29742,29794,29833,29855,29953,30063,30338,30364,30366,30363,30374,64023,30534,21167,30753,30798,30820,30842,31024,64024,64025,64026,31124,64027,31131,31441,31463,64028,31467,31646,64029,32072,32092,32183,32160,32214,32338,32583,32673,64030,33537,33634,33663,33735,33782,33864,33972,34131,34137,34155,64031,34224,64032,64033,34823,35061,35346,35383,35449,35495,35518,35551,64034,35574,35667,35711,36080,36084,36114,36214,64035,36559,64036,64037,36967,37086,64038,37141,37159,37338,37335,37342,37357,37358,37348,37349,37382,37392,37386,37434,37440,37436,37454,37465,37457,37433,37479,37543,37495,37496,37607,37591,37593,37584,64039,37589,37600,37587,37669,37665,37627,64040,37662,37631,37661,37634,37744,37719,37796,37830,37854,37880,37937,37957,37960,38290,63964,64041,38557,38575,38707,38715,38723,38733,38735,38737,38741,38999,39013,64042,64043,39207,64044,39326,39502,39641,39644,39797,39794,39823,39857,39867,39936,40304,40299,64045,40473,40657,null,null,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,65506,65508,65287,65282,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,65506,65508,65287,65282,12849,8470,8481,8757,32394,35100,37704,37512,34012,20425,28859,26161,26824,37625,26363,24389,20008,20193,20220,20224,20227,20281,20310,20370,20362,20378,20372,20429,20544,20514,20479,20510,20550,20592,20546,20628,20724,20696,20810,20836,20893,20926,20972,21013,21148,21158,21184,21211,21248,21255,21284,21362,21395,21426,21469,64014,21660,21642,21673,21759,21894,22361,22373,22444,22472,22471,64015,64016,22686,22706,22795,22867,22875,22877,22883,22948,22970,23382,23488,29999,23512,23532,23582,23718,23738,23797,23847,23891,64017,23874,23917,23992,23993,24016,24353,24372,24423,24503,24542,24669,24709,24714,24798,24789,24864,24818,24849,24887,24880,24984,25107,25254,25589,25696,25757,25806,25934,26112,26133,26171,26121,26158,26142,26148,26213,26199,26201,64018,26227,26265,26272,26290,26303,26362,26382,63785,26470,26555,26706,26560,26625,26692,26831,64019,26984,64020,27032,27106,27184,27243,27206,27251,27262,27362,27364,27606,27711,27740,27782,27759,27866,27908,28039,28015,28054,28076,28111,28152,28146,28156,28217,28252,28199,28220,28351,28552,28597,28661,28677,28679,28712,28805,28843,28943,28932,29020,28998,28999,64021,29121,29182,29361,29374,29476,64022,29559,29629,29641,29654,29667,29650,29703,29685,29734,29738,29737,29742,29794,29833,29855,29953,30063,30338,30364,30366,30363,30374,64023,30534,21167,30753,30798,30820,30842,31024,64024,64025,64026,31124,64027,31131,31441,31463,64028,31467,31646,64029,32072,32092,32183,32160,32214,32338,32583,32673,64030,33537,33634,33663,33735,33782,33864,33972,34131,34137,34155,64031,34224,64032,64033,34823,35061,35346,35383,35449,35495,35518,35551,64034,35574,35667,35711,36080,36084,36114,36214,64035,36559,64036,64037,36967,37086,64038,37141,37159,37338,37335,37342,37357,37358,37348,37349,37382,37392,37386,37434,37440,37436,37454,37465,37457,37433,37479,37543,37495,37496,37607,37591,37593,37584,64039,37589,37600,37587,37669,37665,37627,64040,37662,37631,37661,37634,37744,37719,37796,37830,37854,37880,37937,37957,37960,38290,63964,64041,38557,38575,38707,38715,38723,38733,38735,38737,38741,38999,39013,64042,64043,39207,64044,39326,39502,39641,39644,39797,39794,39823,39857,39867,39936,40304,40299,64045,40473,40657,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null], + "jis0212":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,728,711,184,729,733,175,731,730,65374,900,901,null,null,null,null,null,null,null,null,161,166,191,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,186,170,169,174,8482,164,8470,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,902,904,905,906,938,null,908,null,910,939,null,911,null,null,null,null,940,941,942,943,970,912,972,962,973,971,944,974,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1038,1039,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1118,1119,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,198,272,null,294,null,306,null,321,319,null,330,216,338,null,358,222,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,230,273,240,295,305,307,312,322,320,329,331,248,339,223,359,254,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,193,192,196,194,258,461,256,260,197,195,262,264,268,199,266,270,201,200,203,202,282,278,274,280,null,284,286,290,288,292,205,204,207,206,463,304,298,302,296,308,310,313,317,315,323,327,325,209,211,210,214,212,465,336,332,213,340,344,342,346,348,352,350,356,354,218,217,220,219,364,467,368,362,370,366,360,471,475,473,469,372,221,376,374,377,381,379,null,null,null,null,null,null,null,225,224,228,226,259,462,257,261,229,227,263,265,269,231,267,271,233,232,235,234,283,279,275,281,501,285,287,null,289,293,237,236,239,238,464,null,299,303,297,309,311,314,318,316,324,328,326,241,243,242,246,244,466,337,333,245,341,345,343,347,349,353,351,357,355,250,249,252,251,365,468,369,363,371,367,361,472,476,474,470,373,253,255,375,378,382,380,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,19970,19972,19973,19980,19986,19999,20003,20004,20008,20011,20014,20015,20016,20021,20032,20033,20036,20039,20049,20058,20060,20067,20072,20073,20084,20085,20089,20095,20109,20118,20119,20125,20143,20153,20163,20176,20186,20187,20192,20193,20194,20200,20207,20209,20211,20213,20221,20222,20223,20224,20226,20227,20232,20235,20236,20242,20245,20246,20247,20249,20270,20273,20320,20275,20277,20279,20281,20283,20286,20288,20290,20296,20297,20299,20300,20306,20308,20310,20312,20319,20323,20330,20332,20334,20337,20343,20344,20345,20346,20349,20350,20353,20354,20356,20357,20361,20362,20364,20366,20368,20370,20371,20372,20375,20377,20378,20382,20383,20402,20407,20409,20411,20412,20413,20414,20416,20417,20421,20422,20424,20425,20427,20428,20429,20431,20434,20444,20448,20450,20464,20466,20476,20477,20479,20480,20481,20484,20487,20490,20492,20494,20496,20499,20503,20504,20507,20508,20509,20510,20514,20519,20526,20528,20530,20531,20533,20544,20545,20546,20549,20550,20554,20556,20558,20561,20562,20563,20567,20569,20575,20576,20578,20579,20582,20583,20586,20589,20592,20593,20539,20609,20611,20612,20614,20618,20622,20623,20624,20626,20627,20628,20630,20635,20636,20638,20639,20640,20641,20642,20650,20655,20656,20665,20666,20669,20672,20675,20676,20679,20684,20686,20688,20691,20692,20696,20700,20701,20703,20706,20708,20710,20712,20713,20719,20721,20726,20730,20734,20739,20742,20743,20744,20747,20748,20749,20750,20722,20752,20759,20761,20763,20764,20765,20766,20771,20775,20776,20780,20781,20783,20785,20787,20788,20789,20792,20793,20802,20810,20815,20819,20821,20823,20824,20831,20836,20838,20862,20867,20868,20875,20878,20888,20893,20897,20899,20909,20920,20922,20924,20926,20927,20930,20936,20943,20945,20946,20947,20949,20952,20958,20962,20965,20974,20978,20979,20980,20983,20993,20994,20997,21010,21011,21013,21014,21016,21026,21032,21041,21042,21045,21052,21061,21065,21077,21079,21080,21082,21084,21087,21088,21089,21094,21102,21111,21112,21113,21120,21122,21125,21130,21132,21139,21141,21142,21143,21144,21146,21148,21156,21157,21158,21159,21167,21168,21174,21175,21176,21178,21179,21181,21184,21188,21190,21192,21196,21199,21201,21204,21206,21211,21212,21217,21221,21224,21225,21226,21228,21232,21233,21236,21238,21239,21248,21251,21258,21259,21260,21265,21267,21272,21275,21276,21278,21279,21285,21287,21288,21289,21291,21292,21293,21296,21298,21301,21308,21309,21310,21314,21324,21323,21337,21339,21345,21347,21349,21356,21357,21362,21369,21374,21379,21383,21384,21390,21395,21396,21401,21405,21409,21412,21418,21419,21423,21426,21428,21429,21431,21432,21434,21437,21440,21445,21455,21458,21459,21461,21466,21469,21470,21472,21478,21479,21493,21506,21523,21530,21537,21543,21544,21546,21551,21553,21556,21557,21571,21572,21575,21581,21583,21598,21602,21604,21606,21607,21609,21611,21613,21614,21620,21631,21633,21635,21637,21640,21641,21645,21649,21653,21654,21660,21663,21665,21670,21671,21673,21674,21677,21678,21681,21687,21689,21690,21691,21695,21702,21706,21709,21710,21728,21738,21740,21743,21750,21756,21758,21759,21760,21761,21765,21768,21769,21772,21773,21774,21781,21802,21803,21810,21813,21814,21819,21820,21821,21825,21831,21833,21834,21837,21840,21841,21848,21850,21851,21854,21856,21857,21860,21862,21887,21889,21890,21894,21896,21902,21903,21905,21906,21907,21908,21911,21923,21924,21933,21938,21951,21953,21955,21958,21961,21963,21964,21966,21969,21970,21971,21975,21976,21979,21982,21986,21993,22006,22015,22021,22024,22026,22029,22030,22031,22032,22033,22034,22041,22060,22064,22067,22069,22071,22073,22075,22076,22077,22079,22080,22081,22083,22084,22086,22089,22091,22093,22095,22100,22110,22112,22113,22114,22115,22118,22121,22125,22127,22129,22130,22133,22148,22149,22152,22155,22156,22165,22169,22170,22173,22174,22175,22182,22183,22184,22185,22187,22188,22189,22193,22195,22199,22206,22213,22217,22218,22219,22223,22224,22220,22221,22233,22236,22237,22239,22241,22244,22245,22246,22247,22248,22257,22251,22253,22262,22263,22273,22274,22279,22282,22284,22289,22293,22298,22299,22301,22304,22306,22307,22308,22309,22313,22314,22316,22318,22319,22323,22324,22333,22334,22335,22341,22342,22348,22349,22354,22370,22373,22375,22376,22379,22381,22382,22383,22384,22385,22387,22388,22389,22391,22393,22394,22395,22396,22398,22401,22403,22412,22420,22423,22425,22426,22428,22429,22430,22431,22433,22421,22439,22440,22441,22444,22456,22461,22471,22472,22476,22479,22485,22493,22494,22500,22502,22503,22505,22509,22512,22517,22518,22520,22525,22526,22527,22531,22532,22536,22537,22497,22540,22541,22555,22558,22559,22560,22566,22567,22573,22578,22585,22591,22601,22604,22605,22607,22608,22613,22623,22625,22628,22631,22632,22648,22652,22655,22656,22657,22663,22664,22665,22666,22668,22669,22671,22672,22676,22678,22685,22688,22689,22690,22694,22697,22705,22706,22724,22716,22722,22728,22733,22734,22736,22738,22740,22742,22746,22749,22753,22754,22761,22771,22789,22790,22795,22796,22802,22803,22804,34369,22813,22817,22819,22820,22824,22831,22832,22835,22837,22838,22847,22851,22854,22866,22867,22873,22875,22877,22878,22879,22881,22883,22891,22893,22895,22898,22901,22902,22905,22907,22908,22923,22924,22926,22930,22933,22935,22943,22948,22951,22957,22958,22959,22960,22963,22967,22970,22972,22977,22979,22980,22984,22986,22989,22994,23005,23006,23007,23011,23012,23015,23022,23023,23025,23026,23028,23031,23040,23044,23052,23053,23054,23058,23059,23070,23075,23076,23079,23080,23082,23085,23088,23108,23109,23111,23112,23116,23120,23125,23134,23139,23141,23143,23149,23159,23162,23163,23166,23179,23184,23187,23190,23193,23196,23198,23199,23200,23202,23207,23212,23217,23218,23219,23221,23224,23226,23227,23231,23236,23238,23240,23247,23258,23260,23264,23269,23274,23278,23285,23286,23293,23296,23297,23304,23319,23348,23321,23323,23325,23329,23333,23341,23352,23361,23371,23372,23378,23382,23390,23400,23406,23407,23420,23421,23422,23423,23425,23428,23430,23434,23438,23440,23441,23443,23444,23446,23464,23465,23468,23469,23471,23473,23474,23479,23482,23484,23488,23489,23501,23503,23510,23511,23512,23513,23514,23520,23535,23537,23540,23549,23564,23575,23582,23583,23587,23590,23593,23595,23596,23598,23600,23602,23605,23606,23641,23642,23644,23650,23651,23655,23656,23657,23661,23664,23668,23669,23674,23675,23676,23677,23687,23688,23690,23695,23698,23709,23711,23712,23714,23715,23718,23722,23730,23732,23733,23738,23753,23755,23762,23773,23767,23790,23793,23794,23796,23809,23814,23821,23826,23851,23843,23844,23846,23847,23857,23860,23865,23869,23871,23874,23875,23878,23880,23893,23889,23897,23882,23903,23904,23905,23906,23908,23914,23917,23920,23929,23930,23934,23935,23937,23939,23944,23946,23954,23955,23956,23957,23961,23963,23967,23968,23975,23979,23984,23988,23992,23993,24003,24007,24011,24016,24014,24024,24025,24032,24036,24041,24056,24057,24064,24071,24077,24082,24084,24085,24088,24095,24096,24110,24104,24114,24117,24126,24139,24144,24137,24145,24150,24152,24155,24156,24158,24168,24170,24171,24172,24173,24174,24176,24192,24203,24206,24226,24228,24229,24232,24234,24236,24241,24243,24253,24254,24255,24262,24268,24267,24270,24273,24274,24276,24277,24284,24286,24293,24299,24322,24326,24327,24328,24334,24345,24348,24349,24353,24354,24355,24356,24360,24363,24364,24366,24368,24372,24374,24379,24381,24383,24384,24388,24389,24391,24397,24400,24404,24408,24411,24416,24419,24420,24423,24431,24434,24436,24437,24440,24442,24445,24446,24457,24461,24463,24470,24476,24477,24482,24487,24491,24484,24492,24495,24496,24497,24504,24516,24519,24520,24521,24523,24528,24529,24530,24531,24532,24542,24545,24546,24552,24553,24554,24556,24557,24558,24559,24562,24563,24566,24570,24572,24583,24586,24589,24595,24596,24599,24600,24602,24607,24612,24621,24627,24629,24640,24647,24648,24649,24652,24657,24660,24662,24663,24669,24673,24679,24689,24702,24703,24706,24710,24712,24714,24718,24721,24723,24725,24728,24733,24734,24738,24740,24741,24744,24752,24753,24759,24763,24766,24770,24772,24776,24777,24778,24779,24782,24783,24788,24789,24793,24795,24797,24798,24802,24805,24818,24821,24824,24828,24829,24834,24839,24842,24844,24848,24849,24850,24851,24852,24854,24855,24857,24860,24862,24866,24874,24875,24880,24881,24885,24886,24887,24889,24897,24901,24902,24905,24926,24928,24940,24946,24952,24955,24956,24959,24960,24961,24963,24964,24971,24973,24978,24979,24983,24984,24988,24989,24991,24992,24997,25000,25002,25005,25016,25017,25020,25024,25025,25026,25038,25039,25045,25052,25053,25054,25055,25057,25058,25063,25065,25061,25068,25069,25071,25089,25091,25092,25095,25107,25109,25116,25120,25122,25123,25127,25129,25131,25145,25149,25154,25155,25156,25158,25164,25168,25169,25170,25172,25174,25178,25180,25188,25197,25199,25203,25210,25213,25229,25230,25231,25232,25254,25256,25267,25270,25271,25274,25278,25279,25284,25294,25301,25302,25306,25322,25330,25332,25340,25341,25347,25348,25354,25355,25357,25360,25363,25366,25368,25385,25386,25389,25397,25398,25401,25404,25409,25410,25411,25412,25414,25418,25419,25422,25426,25427,25428,25432,25435,25445,25446,25452,25453,25457,25460,25461,25464,25468,25469,25471,25474,25476,25479,25482,25488,25492,25493,25497,25498,25502,25508,25510,25517,25518,25519,25533,25537,25541,25544,25550,25553,25555,25556,25557,25564,25568,25573,25578,25580,25586,25587,25589,25592,25593,25609,25610,25616,25618,25620,25624,25630,25632,25634,25636,25637,25641,25642,25647,25648,25653,25661,25663,25675,25679,25681,25682,25683,25684,25690,25691,25692,25693,25695,25696,25697,25699,25709,25715,25716,25723,25725,25733,25735,25743,25744,25745,25752,25753,25755,25757,25759,25761,25763,25766,25768,25772,25779,25789,25790,25791,25796,25801,25802,25803,25804,25806,25808,25809,25813,25815,25828,25829,25833,25834,25837,25840,25845,25847,25851,25855,25857,25860,25864,25865,25866,25871,25875,25876,25878,25881,25883,25886,25887,25890,25894,25897,25902,25905,25914,25916,25917,25923,25927,25929,25936,25938,25940,25951,25952,25959,25963,25978,25981,25985,25989,25994,26002,26005,26008,26013,26016,26019,26022,26030,26034,26035,26036,26047,26050,26056,26057,26062,26064,26068,26070,26072,26079,26096,26098,26100,26101,26105,26110,26111,26112,26116,26120,26121,26125,26129,26130,26133,26134,26141,26142,26145,26146,26147,26148,26150,26153,26154,26155,26156,26158,26160,26161,26163,26169,26167,26176,26181,26182,26186,26188,26193,26190,26199,26200,26201,26203,26204,26208,26209,26363,26218,26219,26220,26238,26227,26229,26239,26231,26232,26233,26235,26240,26236,26251,26252,26253,26256,26258,26265,26266,26267,26268,26271,26272,26276,26285,26289,26290,26293,26299,26303,26304,26306,26307,26312,26316,26318,26319,26324,26331,26335,26344,26347,26348,26350,26362,26373,26375,26382,26387,26393,26396,26400,26402,26419,26430,26437,26439,26440,26444,26452,26453,26461,26470,26476,26478,26484,26486,26491,26497,26500,26510,26511,26513,26515,26518,26520,26521,26523,26544,26545,26546,26549,26555,26556,26557,26617,26560,26562,26563,26565,26568,26569,26578,26583,26585,26588,26593,26598,26608,26610,26614,26615,26706,26644,26649,26653,26655,26664,26663,26668,26669,26671,26672,26673,26675,26683,26687,26692,26693,26698,26700,26709,26711,26712,26715,26731,26734,26735,26736,26737,26738,26741,26745,26746,26747,26748,26754,26756,26758,26760,26774,26776,26778,26780,26785,26787,26789,26793,26794,26798,26802,26811,26821,26824,26828,26831,26832,26833,26835,26838,26841,26844,26845,26853,26856,26858,26859,26860,26861,26864,26865,26869,26870,26875,26876,26877,26886,26889,26890,26896,26897,26899,26902,26903,26929,26931,26933,26936,26939,26946,26949,26953,26958,26967,26971,26979,26980,26981,26982,26984,26985,26988,26992,26993,26994,27002,27003,27007,27008,27021,27026,27030,27032,27041,27045,27046,27048,27051,27053,27055,27063,27064,27066,27068,27077,27080,27089,27094,27095,27106,27109,27118,27119,27121,27123,27125,27134,27136,27137,27139,27151,27153,27157,27162,27165,27168,27172,27176,27184,27186,27188,27191,27195,27198,27199,27205,27206,27209,27210,27214,27216,27217,27218,27221,27222,27227,27236,27239,27242,27249,27251,27262,27265,27267,27270,27271,27273,27275,27281,27291,27293,27294,27295,27301,27307,27311,27312,27313,27316,27325,27326,27327,27334,27337,27336,27340,27344,27348,27349,27350,27356,27357,27364,27367,27372,27376,27377,27378,27388,27389,27394,27395,27398,27399,27401,27407,27408,27409,27415,27419,27422,27428,27432,27435,27436,27439,27445,27446,27451,27455,27462,27466,27469,27474,27478,27480,27485,27488,27495,27499,27502,27504,27509,27517,27518,27522,27525,27543,27547,27551,27552,27554,27555,27560,27561,27564,27565,27566,27568,27576,27577,27581,27582,27587,27588,27593,27596,27606,27610,27617,27619,27622,27623,27630,27633,27639,27641,27647,27650,27652,27653,27657,27661,27662,27664,27666,27673,27679,27686,27687,27688,27692,27694,27699,27701,27702,27706,27707,27711,27722,27723,27725,27727,27730,27732,27737,27739,27740,27755,27757,27759,27764,27766,27768,27769,27771,27781,27782,27783,27785,27796,27797,27799,27800,27804,27807,27824,27826,27828,27842,27846,27853,27855,27856,27857,27858,27860,27862,27866,27868,27872,27879,27881,27883,27884,27886,27890,27892,27908,27911,27914,27918,27919,27921,27923,27930,27942,27943,27944,27751,27950,27951,27953,27961,27964,27967,27991,27998,27999,28001,28005,28007,28015,28016,28028,28034,28039,28049,28050,28052,28054,28055,28056,28074,28076,28084,28087,28089,28093,28095,28100,28104,28106,28110,28111,28118,28123,28125,28127,28128,28130,28133,28137,28143,28144,28148,28150,28156,28160,28164,28190,28194,28199,28210,28214,28217,28219,28220,28228,28229,28232,28233,28235,28239,28241,28242,28243,28244,28247,28252,28253,28254,28258,28259,28264,28275,28283,28285,28301,28307,28313,28320,28327,28333,28334,28337,28339,28347,28351,28352,28353,28355,28359,28360,28362,28365,28366,28367,28395,28397,28398,28409,28411,28413,28420,28424,28426,28428,28429,28438,28440,28442,28443,28454,28457,28458,28463,28464,28467,28470,28475,28476,28461,28495,28497,28498,28499,28503,28505,28506,28509,28510,28513,28514,28520,28524,28541,28542,28547,28551,28552,28555,28556,28557,28560,28562,28563,28564,28566,28570,28575,28576,28581,28582,28583,28584,28590,28591,28592,28597,28598,28604,28613,28615,28616,28618,28634,28638,28648,28649,28656,28661,28665,28668,28669,28672,28677,28678,28679,28685,28695,28704,28707,28719,28724,28727,28729,28732,28739,28740,28744,28745,28746,28747,28756,28757,28765,28766,28750,28772,28773,28780,28782,28789,28790,28798,28801,28805,28806,28820,28821,28822,28823,28824,28827,28836,28843,28848,28849,28852,28855,28874,28881,28883,28884,28885,28886,28888,28892,28900,28922,28931,28932,28933,28934,28935,28939,28940,28943,28958,28960,28971,28973,28975,28976,28977,28984,28993,28997,28998,28999,29002,29003,29008,29010,29015,29018,29020,29022,29024,29032,29049,29056,29061,29063,29068,29074,29082,29083,29088,29090,29103,29104,29106,29107,29114,29119,29120,29121,29124,29131,29132,29139,29142,29145,29146,29148,29176,29182,29184,29191,29192,29193,29203,29207,29210,29213,29215,29220,29227,29231,29236,29240,29241,29249,29250,29251,29253,29262,29263,29264,29267,29269,29270,29274,29276,29278,29280,29283,29288,29291,29294,29295,29297,29303,29304,29307,29308,29311,29316,29321,29325,29326,29331,29339,29352,29357,29358,29361,29364,29374,29377,29383,29385,29388,29397,29398,29400,29407,29413,29427,29428,29434,29435,29438,29442,29444,29445,29447,29451,29453,29458,29459,29464,29465,29470,29474,29476,29479,29480,29484,29489,29490,29493,29498,29499,29501,29507,29517,29520,29522,29526,29528,29533,29534,29535,29536,29542,29543,29545,29547,29548,29550,29551,29553,29559,29561,29564,29568,29569,29571,29573,29574,29582,29584,29587,29589,29591,29592,29596,29598,29599,29600,29602,29605,29606,29610,29611,29613,29621,29623,29625,29628,29629,29631,29637,29638,29641,29643,29644,29647,29650,29651,29654,29657,29661,29665,29667,29670,29671,29673,29684,29685,29687,29689,29690,29691,29693,29695,29696,29697,29700,29703,29706,29713,29722,29723,29732,29734,29736,29737,29738,29739,29740,29741,29742,29743,29744,29745,29753,29760,29763,29764,29766,29767,29771,29773,29777,29778,29783,29789,29794,29798,29799,29800,29803,29805,29806,29809,29810,29824,29825,29829,29830,29831,29833,29839,29840,29841,29842,29848,29849,29850,29852,29855,29856,29857,29859,29862,29864,29865,29866,29867,29870,29871,29873,29874,29877,29881,29883,29887,29896,29897,29900,29904,29907,29912,29914,29915,29918,29919,29924,29928,29930,29931,29935,29940,29946,29947,29948,29951,29958,29970,29974,29975,29984,29985,29988,29991,29993,29994,29999,30006,30009,30013,30014,30015,30016,30019,30023,30024,30030,30032,30034,30039,30046,30047,30049,30063,30065,30073,30074,30075,30076,30077,30078,30081,30085,30096,30098,30099,30101,30105,30108,30114,30116,30132,30138,30143,30144,30145,30148,30150,30156,30158,30159,30167,30172,30175,30176,30177,30180,30183,30188,30190,30191,30193,30201,30208,30210,30211,30212,30215,30216,30218,30220,30223,30226,30227,30229,30230,30233,30235,30236,30237,30238,30243,30245,30246,30249,30253,30258,30259,30261,30264,30265,30266,30268,30282,30272,30273,30275,30276,30277,30281,30283,30293,30297,30303,30308,30309,30317,30318,30319,30321,30324,30337,30341,30348,30349,30357,30363,30364,30365,30367,30368,30370,30371,30372,30373,30374,30375,30376,30378,30381,30397,30401,30405,30409,30411,30412,30414,30420,30425,30432,30438,30440,30444,30448,30449,30454,30457,30460,30464,30470,30474,30478,30482,30484,30485,30487,30489,30490,30492,30498,30504,30509,30510,30511,30516,30517,30518,30521,30525,30526,30530,30533,30534,30538,30541,30542,30543,30546,30550,30551,30556,30558,30559,30560,30562,30564,30567,30570,30572,30576,30578,30579,30580,30586,30589,30592,30596,30604,30605,30612,30613,30614,30618,30623,30626,30631,30634,30638,30639,30641,30645,30654,30659,30665,30673,30674,30677,30681,30686,30687,30688,30692,30694,30698,30700,30704,30705,30708,30712,30715,30725,30726,30729,30733,30734,30737,30749,30753,30754,30755,30765,30766,30768,30773,30775,30787,30788,30791,30792,30796,30798,30802,30812,30814,30816,30817,30819,30820,30824,30826,30830,30842,30846,30858,30863,30868,30872,30881,30877,30878,30879,30884,30888,30892,30893,30896,30897,30898,30899,30907,30909,30911,30919,30920,30921,30924,30926,30930,30931,30933,30934,30948,30939,30943,30944,30945,30950,30954,30962,30963,30976,30966,30967,30970,30971,30975,30982,30988,30992,31002,31004,31006,31007,31008,31013,31015,31017,31021,31025,31028,31029,31035,31037,31039,31044,31045,31046,31050,31051,31055,31057,31060,31064,31067,31068,31079,31081,31083,31090,31097,31099,31100,31102,31115,31116,31121,31123,31124,31125,31126,31128,31131,31132,31137,31144,31145,31147,31151,31153,31156,31160,31163,31170,31172,31175,31176,31178,31183,31188,31190,31194,31197,31198,31200,31202,31205,31210,31211,31213,31217,31224,31228,31234,31235,31239,31241,31242,31244,31249,31253,31259,31262,31265,31271,31275,31277,31279,31280,31284,31285,31288,31289,31290,31300,31301,31303,31304,31308,31317,31318,31321,31324,31325,31327,31328,31333,31335,31338,31341,31349,31352,31358,31360,31362,31365,31366,31370,31371,31376,31377,31380,31390,31392,31395,31404,31411,31413,31417,31419,31420,31430,31433,31436,31438,31441,31451,31464,31465,31467,31468,31473,31476,31483,31485,31486,31495,31508,31519,31523,31527,31529,31530,31531,31533,31534,31535,31536,31537,31540,31549,31551,31552,31553,31559,31566,31573,31584,31588,31590,31593,31594,31597,31599,31602,31603,31607,31620,31625,31630,31632,31633,31638,31643,31646,31648,31653,31660,31663,31664,31666,31669,31670,31674,31675,31676,31677,31682,31685,31688,31690,31700,31702,31703,31705,31706,31707,31720,31722,31730,31732,31733,31736,31737,31738,31740,31742,31745,31746,31747,31748,31750,31753,31755,31756,31758,31759,31769,31771,31776,31781,31782,31784,31788,31793,31795,31796,31798,31801,31802,31814,31818,31829,31825,31826,31827,31833,31834,31835,31836,31837,31838,31841,31843,31847,31849,31853,31854,31856,31858,31865,31868,31869,31878,31879,31887,31892,31902,31904,31910,31920,31926,31927,31930,31931,31932,31935,31940,31943,31944,31945,31949,31951,31955,31956,31957,31959,31961,31962,31965,31974,31977,31979,31989,32003,32007,32008,32009,32015,32017,32018,32019,32022,32029,32030,32035,32038,32042,32045,32049,32060,32061,32062,32064,32065,32071,32072,32077,32081,32083,32087,32089,32090,32092,32093,32101,32103,32106,32112,32120,32122,32123,32127,32129,32130,32131,32133,32134,32136,32139,32140,32141,32145,32150,32151,32157,32158,32166,32167,32170,32179,32182,32183,32185,32194,32195,32196,32197,32198,32204,32205,32206,32215,32217,32256,32226,32229,32230,32234,32235,32237,32241,32245,32246,32249,32250,32264,32272,32273,32277,32279,32284,32285,32288,32295,32296,32300,32301,32303,32307,32310,32319,32324,32325,32327,32334,32336,32338,32344,32351,32353,32354,32357,32363,32366,32367,32371,32376,32382,32385,32390,32391,32394,32397,32401,32405,32408,32410,32413,32414,32572,32571,32573,32574,32575,32579,32580,32583,32591,32594,32595,32603,32604,32605,32609,32611,32612,32613,32614,32621,32625,32637,32638,32639,32640,32651,32653,32655,32656,32657,32662,32663,32668,32673,32674,32678,32682,32685,32692,32700,32703,32704,32707,32712,32718,32719,32731,32735,32739,32741,32744,32748,32750,32751,32754,32762,32765,32766,32767,32775,32776,32778,32781,32782,32783,32785,32787,32788,32790,32797,32798,32799,32800,32804,32806,32812,32814,32816,32820,32821,32823,32825,32826,32828,32830,32832,32836,32864,32868,32870,32877,32881,32885,32897,32904,32910,32924,32926,32934,32935,32939,32952,32953,32968,32973,32975,32978,32980,32981,32983,32984,32992,33005,33006,33008,33010,33011,33014,33017,33018,33022,33027,33035,33046,33047,33048,33052,33054,33056,33060,33063,33068,33072,33077,33082,33084,33093,33095,33098,33100,33106,33111,33120,33121,33127,33128,33129,33133,33135,33143,33153,33168,33156,33157,33158,33163,33166,33174,33176,33179,33182,33186,33198,33202,33204,33211,33227,33219,33221,33226,33230,33231,33237,33239,33243,33245,33246,33249,33252,33259,33260,33264,33265,33266,33269,33270,33272,33273,33277,33279,33280,33283,33295,33299,33300,33305,33306,33309,33313,33314,33320,33330,33332,33338,33347,33348,33349,33350,33355,33358,33359,33361,33366,33372,33376,33379,33383,33389,33396,33403,33405,33407,33408,33409,33411,33412,33415,33417,33418,33422,33425,33428,33430,33432,33434,33435,33440,33441,33443,33444,33447,33448,33449,33450,33454,33456,33458,33460,33463,33466,33468,33470,33471,33478,33488,33493,33498,33504,33506,33508,33512,33514,33517,33519,33526,33527,33533,33534,33536,33537,33543,33544,33546,33547,33620,33563,33565,33566,33567,33569,33570,33580,33581,33582,33584,33587,33591,33594,33596,33597,33602,33603,33604,33607,33613,33614,33617,33621,33622,33623,33648,33656,33661,33663,33664,33666,33668,33670,33677,33682,33684,33685,33688,33689,33691,33692,33693,33702,33703,33705,33708,33726,33727,33728,33735,33737,33743,33744,33745,33748,33757,33619,33768,33770,33782,33784,33785,33788,33793,33798,33802,33807,33809,33813,33817,33709,33839,33849,33861,33863,33864,33866,33869,33871,33873,33874,33878,33880,33881,33882,33884,33888,33892,33893,33895,33898,33904,33907,33908,33910,33912,33916,33917,33921,33925,33938,33939,33941,33950,33958,33960,33961,33962,33967,33969,33972,33978,33981,33982,33984,33986,33991,33992,33996,33999,34003,34012,34023,34026,34031,34032,34033,34034,34039,34098,34042,34043,34045,34050,34051,34055,34060,34062,34064,34076,34078,34082,34083,34084,34085,34087,34090,34091,34095,34099,34100,34102,34111,34118,34127,34128,34129,34130,34131,34134,34137,34140,34141,34142,34143,34144,34145,34146,34148,34155,34159,34169,34170,34171,34173,34175,34177,34181,34182,34185,34187,34188,34191,34195,34200,34205,34207,34208,34210,34213,34215,34228,34230,34231,34232,34236,34237,34238,34239,34242,34247,34250,34251,34254,34221,34264,34266,34271,34272,34278,34280,34285,34291,34294,34300,34303,34304,34308,34309,34317,34318,34320,34321,34322,34328,34329,34331,34334,34337,34343,34345,34358,34360,34362,34364,34365,34368,34370,34374,34386,34387,34390,34391,34392,34393,34397,34400,34401,34402,34403,34404,34409,34412,34415,34421,34422,34423,34426,34445,34449,34454,34456,34458,34460,34465,34470,34471,34472,34477,34481,34483,34484,34485,34487,34488,34489,34495,34496,34497,34499,34501,34513,34514,34517,34519,34522,34524,34528,34531,34533,34535,34440,34554,34556,34557,34564,34565,34567,34571,34574,34575,34576,34579,34580,34585,34590,34591,34593,34595,34600,34606,34607,34609,34610,34617,34618,34620,34621,34622,34624,34627,34629,34637,34648,34653,34657,34660,34661,34671,34673,34674,34683,34691,34692,34693,34694,34695,34696,34697,34699,34700,34704,34707,34709,34711,34712,34713,34718,34720,34723,34727,34732,34733,34734,34737,34741,34750,34751,34753,34760,34761,34762,34766,34773,34774,34777,34778,34780,34783,34786,34787,34788,34794,34795,34797,34801,34803,34808,34810,34815,34817,34819,34822,34825,34826,34827,34832,34841,34834,34835,34836,34840,34842,34843,34844,34846,34847,34856,34861,34862,34864,34866,34869,34874,34876,34881,34883,34885,34888,34889,34890,34891,34894,34897,34901,34902,34904,34906,34908,34911,34912,34916,34921,34929,34937,34939,34944,34968,34970,34971,34972,34975,34976,34984,34986,35002,35005,35006,35008,35018,35019,35020,35021,35022,35025,35026,35027,35035,35038,35047,35055,35056,35057,35061,35063,35073,35078,35085,35086,35087,35093,35094,35096,35097,35098,35100,35104,35110,35111,35112,35120,35121,35122,35125,35129,35130,35134,35136,35138,35141,35142,35145,35151,35154,35159,35162,35163,35164,35169,35170,35171,35179,35182,35184,35187,35189,35194,35195,35196,35197,35209,35213,35216,35220,35221,35227,35228,35231,35232,35237,35248,35252,35253,35254,35255,35260,35284,35285,35286,35287,35288,35301,35305,35307,35309,35313,35315,35318,35321,35325,35327,35332,35333,35335,35343,35345,35346,35348,35349,35358,35360,35362,35364,35366,35371,35372,35375,35381,35383,35389,35390,35392,35395,35397,35399,35401,35405,35406,35411,35414,35415,35416,35420,35421,35425,35429,35431,35445,35446,35447,35449,35450,35451,35454,35455,35456,35459,35462,35467,35471,35472,35474,35478,35479,35481,35487,35495,35497,35502,35503,35507,35510,35511,35515,35518,35523,35526,35528,35529,35530,35537,35539,35540,35541,35543,35549,35551,35564,35568,35572,35573,35574,35580,35583,35589,35590,35595,35601,35612,35614,35615,35594,35629,35632,35639,35644,35650,35651,35652,35653,35654,35656,35666,35667,35668,35673,35661,35678,35683,35693,35702,35704,35705,35708,35710,35713,35716,35717,35723,35725,35727,35732,35733,35740,35742,35743,35896,35897,35901,35902,35909,35911,35913,35915,35919,35921,35923,35924,35927,35928,35931,35933,35929,35939,35940,35942,35944,35945,35949,35955,35957,35958,35963,35966,35974,35975,35979,35984,35986,35987,35993,35995,35996,36004,36025,36026,36037,36038,36041,36043,36047,36054,36053,36057,36061,36065,36072,36076,36079,36080,36082,36085,36087,36088,36094,36095,36097,36099,36105,36114,36119,36123,36197,36201,36204,36206,36223,36226,36228,36232,36237,36240,36241,36245,36254,36255,36256,36262,36267,36268,36271,36274,36277,36279,36281,36283,36288,36293,36294,36295,36296,36298,36302,36305,36308,36309,36311,36313,36324,36325,36327,36332,36336,36284,36337,36338,36340,36349,36353,36356,36357,36358,36363,36369,36372,36374,36384,36385,36386,36387,36390,36391,36401,36403,36406,36407,36408,36409,36413,36416,36417,36427,36429,36430,36431,36436,36443,36444,36445,36446,36449,36450,36457,36460,36461,36463,36464,36465,36473,36474,36475,36482,36483,36489,36496,36498,36501,36506,36507,36509,36510,36514,36519,36521,36525,36526,36531,36533,36538,36539,36544,36545,36547,36548,36551,36559,36561,36564,36572,36584,36590,36592,36593,36599,36601,36602,36589,36608,36610,36615,36616,36623,36624,36630,36631,36632,36638,36640,36641,36643,36645,36647,36648,36652,36653,36654,36660,36661,36662,36663,36666,36672,36673,36675,36679,36687,36689,36690,36691,36692,36693,36696,36701,36702,36709,36765,36768,36769,36772,36773,36774,36789,36790,36792,36798,36800,36801,36806,36810,36811,36813,36816,36818,36819,36821,36832,36835,36836,36840,36846,36849,36853,36854,36859,36862,36866,36868,36872,36876,36888,36891,36904,36905,36911,36906,36908,36909,36915,36916,36919,36927,36931,36932,36940,36955,36957,36962,36966,36967,36972,36976,36980,36985,36997,37000,37003,37004,37006,37008,37013,37015,37016,37017,37019,37024,37025,37026,37029,37040,37042,37043,37044,37046,37053,37068,37054,37059,37060,37061,37063,37064,37077,37079,37080,37081,37084,37085,37087,37093,37074,37110,37099,37103,37104,37108,37118,37119,37120,37124,37125,37126,37128,37133,37136,37140,37142,37143,37144,37146,37148,37150,37152,37157,37154,37155,37159,37161,37166,37167,37169,37172,37174,37175,37177,37178,37180,37181,37187,37191,37192,37199,37203,37207,37209,37210,37211,37217,37220,37223,37229,37236,37241,37242,37243,37249,37251,37253,37254,37258,37262,37265,37267,37268,37269,37272,37278,37281,37286,37288,37292,37293,37294,37296,37297,37298,37299,37302,37307,37308,37309,37311,37314,37315,37317,37331,37332,37335,37337,37338,37342,37348,37349,37353,37354,37356,37357,37358,37359,37360,37361,37367,37369,37371,37373,37376,37377,37380,37381,37382,37383,37385,37386,37388,37392,37394,37395,37398,37400,37404,37405,37411,37412,37413,37414,37416,37422,37423,37424,37427,37429,37430,37432,37433,37434,37436,37438,37440,37442,37443,37446,37447,37450,37453,37454,37455,37457,37464,37465,37468,37469,37472,37473,37477,37479,37480,37481,37486,37487,37488,37493,37494,37495,37496,37497,37499,37500,37501,37503,37512,37513,37514,37517,37518,37522,37527,37529,37535,37536,37540,37541,37543,37544,37547,37551,37554,37558,37560,37562,37563,37564,37565,37567,37568,37569,37570,37571,37573,37574,37575,37576,37579,37580,37581,37582,37584,37587,37589,37591,37592,37593,37596,37597,37599,37600,37601,37603,37605,37607,37608,37612,37614,37616,37625,37627,37631,37632,37634,37640,37645,37649,37652,37653,37660,37661,37662,37663,37665,37668,37669,37671,37673,37674,37683,37684,37686,37687,37703,37704,37705,37712,37713,37714,37717,37719,37720,37722,37726,37732,37733,37735,37737,37738,37741,37743,37744,37745,37747,37748,37750,37754,37757,37759,37760,37761,37762,37768,37770,37771,37773,37775,37778,37781,37784,37787,37790,37793,37795,37796,37798,37800,37803,37812,37813,37814,37818,37801,37825,37828,37829,37830,37831,37833,37834,37835,37836,37837,37843,37849,37852,37854,37855,37858,37862,37863,37881,37879,37880,37882,37883,37885,37889,37890,37892,37896,37897,37901,37902,37903,37909,37910,37911,37919,37934,37935,37937,37938,37939,37940,37947,37951,37949,37955,37957,37960,37962,37964,37973,37977,37980,37983,37985,37987,37992,37995,37997,37998,37999,38001,38002,38020,38019,38264,38265,38270,38276,38280,38284,38285,38286,38301,38302,38303,38305,38310,38313,38315,38316,38324,38326,38330,38333,38335,38342,38344,38345,38347,38352,38353,38354,38355,38361,38362,38365,38366,38367,38368,38372,38374,38429,38430,38434,38436,38437,38438,38444,38449,38451,38455,38456,38457,38458,38460,38461,38465,38482,38484,38486,38487,38488,38497,38510,38516,38523,38524,38526,38527,38529,38530,38531,38532,38537,38545,38550,38554,38557,38559,38564,38565,38566,38569,38574,38575,38579,38586,38602,38610,23986,38616,38618,38621,38622,38623,38633,38639,38641,38650,38658,38659,38661,38665,38682,38683,38685,38689,38690,38691,38696,38705,38707,38721,38723,38730,38734,38735,38741,38743,38744,38746,38747,38755,38759,38762,38766,38771,38774,38775,38776,38779,38781,38783,38784,38793,38805,38806,38807,38809,38810,38814,38815,38818,38828,38830,38833,38834,38837,38838,38840,38841,38842,38844,38846,38847,38849,38852,38853,38855,38857,38858,38860,38861,38862,38864,38865,38868,38871,38872,38873,38877,38878,38880,38875,38881,38884,38895,38897,38900,38903,38904,38906,38919,38922,38937,38925,38926,38932,38934,38940,38942,38944,38947,38950,38955,38958,38959,38960,38962,38963,38965,38949,38974,38980,38983,38986,38993,38994,38995,38998,38999,39001,39002,39010,39011,39013,39014,39018,39020,39083,39085,39086,39088,39092,39095,39096,39098,39099,39103,39106,39109,39112,39116,39137,39139,39141,39142,39143,39146,39155,39158,39170,39175,39176,39185,39189,39190,39191,39194,39195,39196,39199,39202,39206,39207,39211,39217,39218,39219,39220,39221,39225,39226,39227,39228,39232,39233,39238,39239,39240,39245,39246,39252,39256,39257,39259,39260,39262,39263,39264,39323,39325,39327,39334,39344,39345,39346,39349,39353,39354,39357,39359,39363,39369,39379,39380,39385,39386,39388,39390,39399,39402,39403,39404,39408,39412,39413,39417,39421,39422,39426,39427,39428,39435,39436,39440,39441,39446,39454,39456,39458,39459,39460,39463,39469,39470,39475,39477,39478,39480,39495,39489,39492,39498,39499,39500,39502,39505,39508,39510,39517,39594,39596,39598,39599,39602,39604,39605,39606,39609,39611,39614,39615,39617,39619,39622,39624,39630,39632,39634,39637,39638,39639,39643,39644,39648,39652,39653,39655,39657,39660,39666,39667,39669,39673,39674,39677,39679,39680,39681,39682,39683,39684,39685,39688,39689,39691,39692,39693,39694,39696,39698,39702,39705,39707,39708,39712,39718,39723,39725,39731,39732,39733,39735,39737,39738,39741,39752,39755,39756,39765,39766,39767,39771,39774,39777,39779,39781,39782,39784,39786,39787,39788,39789,39790,39795,39797,39799,39800,39801,39807,39808,39812,39813,39814,39815,39817,39818,39819,39821,39823,39824,39828,39834,39837,39838,39846,39847,39849,39852,39856,39857,39858,39863,39864,39867,39868,39870,39871,39873,39879,39880,39886,39888,39895,39896,39901,39903,39909,39911,39914,39915,39919,39923,39927,39928,39929,39930,39933,39935,39936,39938,39947,39951,39953,39958,39960,39961,39962,39964,39966,39970,39971,39974,39975,39976,39977,39978,39985,39989,39990,39991,39997,40001,40003,40004,40005,40009,40010,40014,40015,40016,40019,40020,40022,40024,40027,40029,40030,40031,40035,40041,40042,40028,40043,40040,40046,40048,40050,40053,40055,40059,40166,40178,40183,40185,40203,40194,40209,40215,40216,40220,40221,40222,40239,40240,40242,40243,40244,40250,40252,40261,40253,40258,40259,40263,40266,40275,40276,40287,40291,40290,40293,40297,40298,40299,40304,40310,40311,40315,40316,40318,40323,40324,40326,40330,40333,40334,40338,40339,40341,40342,40343,40344,40353,40362,40364,40366,40369,40373,40377,40380,40383,40387,40391,40393,40394,40404,40405,40406,40407,40410,40414,40415,40416,40421,40423,40425,40427,40430,40432,40435,40436,40446,40458,40450,40455,40462,40464,40465,40466,40469,40470,40473,40476,40477,40570,40571,40572,40576,40578,40579,40580,40581,40583,40590,40591,40598,40600,40603,40606,40612,40616,40620,40622,40623,40624,40627,40628,40629,40646,40648,40651,40661,40671,40676,40679,40684,40685,40686,40688,40689,40690,40693,40696,40703,40706,40707,40713,40719,40720,40721,40722,40724,40726,40727,40729,40730,40731,40735,40738,40742,40746,40747,40751,40753,40754,40756,40759,40761,40762,40764,40765,40767,40769,40771,40772,40773,40774,40775,40787,40789,40790,40791,40792,40794,40797,40798,40808,40809,40813,40814,40815,40816,40817,40819,40821,40826,40829,40847,40848,40849,40850,40852,40854,40855,40862,40865,40866,40867,40869,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null], + "ibm866":[1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,9617,9618,9619,9474,9508,9569,9570,9558,9557,9571,9553,9559,9565,9564,9563,9488,9492,9524,9516,9500,9472,9532,9566,9567,9562,9556,9577,9574,9568,9552,9580,9575,9576,9572,9573,9561,9560,9554,9555,9579,9578,9496,9484,9608,9604,9612,9616,9600,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1025,1105,1028,1108,1031,1111,1038,1118,176,8729,183,8730,8470,164,9632,160], + "iso-8859-2":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,728,321,164,317,346,167,168,352,350,356,377,173,381,379,176,261,731,322,180,318,347,711,184,353,351,357,378,733,382,380,340,193,194,258,196,313,262,199,268,201,280,203,282,205,206,270,272,323,327,211,212,336,214,215,344,366,218,368,220,221,354,223,341,225,226,259,228,314,263,231,269,233,281,235,283,237,238,271,273,324,328,243,244,337,246,247,345,367,250,369,252,253,355,729], + "iso-8859-3":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,294,728,163,164,null,292,167,168,304,350,286,308,173,null,379,176,295,178,179,180,181,293,183,184,305,351,287,309,189,null,380,192,193,194,null,196,266,264,199,200,201,202,203,204,205,206,207,null,209,210,211,212,288,214,215,284,217,218,219,220,364,348,223,224,225,226,null,228,267,265,231,232,233,234,235,236,237,238,239,null,241,242,243,244,289,246,247,285,249,250,251,252,365,349,729], + "iso-8859-4":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,312,342,164,296,315,167,168,352,274,290,358,173,381,175,176,261,731,343,180,297,316,711,184,353,275,291,359,330,382,331,256,193,194,195,196,197,198,302,268,201,280,203,278,205,206,298,272,325,332,310,212,213,214,215,216,370,218,219,220,360,362,223,257,225,226,227,228,229,230,303,269,233,281,235,279,237,238,299,273,326,333,311,244,245,246,247,248,371,250,251,252,361,363,729], + "iso-8859-5":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,173,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,8470,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,167,1118,1119], + "iso-8859-6":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,null,null,null,164,null,null,null,null,null,null,null,1548,173,null,null,null,null,null,null,null,null,null,null,null,null,null,1563,null,null,null,1567,null,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,null,null,null,null,null,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,null,null,null,null,null,null,null,null,null,null,null,null,null], + "iso-8859-7":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,8216,8217,163,8364,8367,166,167,168,169,890,171,172,173,null,8213,176,177,178,179,900,901,902,183,904,905,906,187,908,189,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,null,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,null], + "iso-8859-8":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,null,162,163,164,165,166,167,168,169,215,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,247,187,188,189,190,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,8215,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,null,null,8206,8207,null], + "iso-8859-10":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,274,290,298,296,310,167,315,272,352,358,381,173,362,330,176,261,275,291,299,297,311,183,316,273,353,359,382,8213,363,331,256,193,194,195,196,197,198,302,268,201,280,203,278,205,206,207,208,325,332,211,212,213,214,360,216,370,218,219,220,221,222,223,257,225,226,227,228,229,230,303,269,233,281,235,279,237,238,239,240,326,333,243,244,245,246,361,248,371,250,251,252,253,254,312], + "iso-8859-13":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,8221,162,163,164,8222,166,167,216,169,342,171,172,173,174,198,176,177,178,179,8220,181,182,183,248,185,343,187,188,189,190,230,260,302,256,262,196,197,280,274,268,201,377,278,290,310,298,315,352,323,325,211,332,213,214,215,370,321,346,362,220,379,381,223,261,303,257,263,228,229,281,275,269,233,378,279,291,311,299,316,353,324,326,243,333,245,246,247,371,322,347,363,252,380,382,8217], + "iso-8859-14":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,7682,7683,163,266,267,7690,167,7808,169,7810,7691,7922,173,174,376,7710,7711,288,289,7744,7745,182,7766,7809,7767,7811,7776,7923,7812,7813,7777,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,372,209,210,211,212,213,214,7786,216,217,218,219,220,221,374,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,373,241,242,243,244,245,246,7787,248,249,250,251,252,253,375,255], + "iso-8859-15":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,8364,165,352,167,353,169,170,171,172,173,174,175,176,177,178,179,381,181,182,183,382,185,186,187,338,339,376,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255], + "iso-8859-16":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,261,321,8364,8222,352,167,353,169,536,171,377,173,378,379,176,177,268,322,381,8221,182,183,382,269,537,187,338,339,376,380,192,193,194,258,196,262,198,199,200,201,202,203,204,205,206,207,272,323,210,211,212,336,214,346,368,217,218,219,220,280,538,223,224,225,226,259,228,263,230,231,232,233,234,235,236,237,238,239,273,324,242,243,244,337,246,347,369,249,250,251,252,281,539,255], + "koi8-r":[9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9600,9604,9608,9612,9616,9617,9618,9619,8992,9632,8729,8730,8776,8804,8805,160,8993,176,178,183,247,9552,9553,9554,1105,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,1025,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,169,1102,1072,1073,1094,1076,1077,1092,1075,1093,1080,1081,1082,1083,1084,1085,1086,1087,1103,1088,1089,1090,1091,1078,1074,1100,1099,1079,1096,1101,1097,1095,1098,1070,1040,1041,1062,1044,1045,1060,1043,1061,1048,1049,1050,1051,1052,1053,1054,1055,1071,1056,1057,1058,1059,1046,1042,1068,1067,1047,1064,1069,1065,1063,1066], + "koi8-u":[9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9600,9604,9608,9612,9616,9617,9618,9619,8992,9632,8729,8730,8776,8804,8805,160,8993,176,178,183,247,9552,9553,9554,1105,1108,9556,1110,1111,9559,9560,9561,9562,9563,1169,1118,9566,9567,9568,9569,1025,1028,9571,1030,1031,9574,9575,9576,9577,9578,1168,1038,169,1102,1072,1073,1094,1076,1077,1092,1075,1093,1080,1081,1082,1083,1084,1085,1086,1087,1103,1088,1089,1090,1091,1078,1074,1100,1099,1079,1096,1101,1097,1095,1098,1070,1040,1041,1062,1044,1045,1060,1043,1061,1048,1049,1050,1051,1052,1053,1054,1055,1071,1056,1057,1058,1059,1046,1042,1068,1067,1047,1064,1069,1065,1063,1066], + "macintosh":[196,197,199,201,209,214,220,225,224,226,228,227,229,231,233,232,234,235,237,236,238,239,241,243,242,244,246,245,250,249,251,252,8224,176,162,163,167,8226,182,223,174,169,8482,180,168,8800,198,216,8734,177,8804,8805,165,181,8706,8721,8719,960,8747,170,186,937,230,248,191,161,172,8730,402,8776,8710,171,187,8230,160,192,195,213,338,339,8211,8212,8220,8221,8216,8217,247,9674,255,376,8260,8364,8249,8250,64257,64258,8225,183,8218,8222,8240,194,202,193,203,200,205,206,207,204,211,212,63743,210,218,219,217,305,710,732,175,728,729,730,184,733,731,711], + "windows-874":[8364,129,130,131,132,8230,134,135,136,137,138,139,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,152,153,154,155,156,157,158,159,160,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,null,null,null,null,3647,3648,3649,3650,3651,3652,3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,null,null,null,null], + "windows-1250":[8364,129,8218,131,8222,8230,8224,8225,136,8240,352,8249,346,356,381,377,144,8216,8217,8220,8221,8226,8211,8212,152,8482,353,8250,347,357,382,378,160,711,728,321,164,260,166,167,168,169,350,171,172,173,174,379,176,177,731,322,180,181,182,183,184,261,351,187,317,733,318,380,340,193,194,258,196,313,262,199,268,201,280,203,282,205,206,270,272,323,327,211,212,336,214,215,344,366,218,368,220,221,354,223,341,225,226,259,228,314,263,231,269,233,281,235,283,237,238,271,273,324,328,243,244,337,246,247,345,367,250,369,252,253,355,729], + "windows-1251":[1026,1027,8218,1107,8222,8230,8224,8225,8364,8240,1033,8249,1034,1036,1035,1039,1106,8216,8217,8220,8221,8226,8211,8212,152,8482,1113,8250,1114,1116,1115,1119,160,1038,1118,1032,164,1168,166,167,1025,169,1028,171,172,173,174,1031,176,177,1030,1110,1169,181,182,183,1105,8470,1108,187,1112,1029,1109,1111,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103], + "windows-1252":[8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,381,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,382,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255], + "windows-1253":[8364,129,8218,402,8222,8230,8224,8225,136,8240,138,8249,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,152,8482,154,8250,156,157,158,159,160,901,902,163,164,165,166,167,168,169,null,171,172,173,174,8213,176,177,178,179,900,181,182,183,904,905,906,187,908,189,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,null,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,null], + "windows-1254":[8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,158,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,286,209,210,211,212,213,214,215,216,217,218,219,220,304,350,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,287,241,242,243,244,245,246,247,248,249,250,251,252,305,351,255], + "windows-1255":[8364,129,8218,402,8222,8230,8224,8225,710,8240,138,8249,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,154,8250,156,157,158,159,160,161,162,163,8362,165,166,167,168,169,215,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,247,187,188,189,190,191,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1520,1521,1522,1523,1524,null,null,null,null,null,null,null,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,null,null,8206,8207,null], + "windows-1256":[8364,1662,8218,402,8222,8230,8224,8225,710,8240,1657,8249,338,1670,1688,1672,1711,8216,8217,8220,8221,8226,8211,8212,1705,8482,1681,8250,339,8204,8205,1722,160,1548,162,163,164,165,166,167,168,169,1726,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,1563,187,188,189,190,1567,1729,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,215,1591,1592,1593,1594,1600,1601,1602,1603,224,1604,226,1605,1606,1607,1608,231,232,233,234,235,1609,1610,238,239,1611,1612,1613,1614,244,1615,1616,247,1617,249,1618,251,252,8206,8207,1746], + "windows-1257":[8364,129,8218,131,8222,8230,8224,8225,136,8240,138,8249,140,168,711,184,144,8216,8217,8220,8221,8226,8211,8212,152,8482,154,8250,156,175,731,159,160,null,162,163,164,null,166,167,216,169,342,171,172,173,174,198,176,177,178,179,180,181,182,183,248,185,343,187,188,189,190,230,260,302,256,262,196,197,280,274,268,201,377,278,290,310,298,315,352,323,325,211,332,213,214,215,370,321,346,362,220,379,381,223,261,303,257,263,228,229,281,275,269,233,378,279,291,311,299,316,353,324,326,243,333,245,246,247,371,322,347,363,252,380,382,729], + "windows-1258":[8364,129,8218,402,8222,8230,8224,8225,710,8240,138,8249,338,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,154,8250,339,157,158,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,258,196,197,198,199,200,201,202,203,768,205,206,207,272,209,777,211,212,416,214,215,216,217,218,219,220,431,771,223,224,225,226,259,228,229,230,231,232,233,234,235,769,237,238,239,273,241,803,243,244,417,246,247,248,249,250,251,252,432,8363,255], + "x-mac-cyrillic":[1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,8224,176,1168,163,167,8226,182,1030,174,169,8482,1026,1106,8800,1027,1107,8734,177,8804,8805,1110,181,1169,1032,1028,1108,1031,1111,1033,1113,1034,1114,1112,1029,172,8730,402,8776,8710,171,187,8230,160,1035,1115,1036,1116,1109,8211,8212,8220,8221,8216,8217,247,8222,1038,1118,1039,1119,8470,1025,1105,1103,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,8364] +} +;}(this)); diff --git a/vendor/assets/javascripts/xterm/encoding.js b/vendor/assets/javascripts/xterm/encoding.js new file mode 100644 index 0000000000000000000000000000000000000000..b5c8904f5a39bff1e2993b97e69b5fb40d3d96a5 --- /dev/null +++ b/vendor/assets/javascripts/xterm/encoding.js @@ -0,0 +1,3309 @@ +// This is free and unencumbered software released into the public domain. +// See LICENSE.md for more information. + +// If we're in node require encoding-indexes and attach it to the global. +/** + * @fileoverview Global |this| required for resolving indexes in node. + * @suppress {globalThis} + */ +if (typeof module !== "undefined" && module.exports && + !this["encoding-indexes"]) { + require("./encoding-indexes.js"); +} + +(function(global) { + 'use strict'; + + // + // Utilities + // + + /** + * @param {number} a The number to test. + * @param {number} min The minimum value in the range, inclusive. + * @param {number} max The maximum value in the range, inclusive. + * @return {boolean} True if a >= min and a <= max. + */ + function inRange(a, min, max) { + return min <= a && a <= max; + } + + /** + * @param {!Array.<*>} array The array to check. + * @param {*} item The item to look for in the array. + * @return {boolean} True if the item appears in the array. + */ + function includes(array, item) { + return array.indexOf(item) !== -1; + } + + var floor = Math.floor; + + /** + * @param {*} o + * @return {Object} + */ + function ToDictionary(o) { + if (o === undefined) return {}; + if (o === Object(o)) return o; + throw TypeError('Could not convert argument to dictionary'); + } + + /** + * @param {string} string Input string of UTF-16 code units. + * @return {!Array.<number>} Code points. + */ + function stringToCodePoints(string) { + // https://heycam.github.io/webidl/#dfn-obtain-unicode + + // 1. Let S be the DOMString value. + var s = String(string); + + // 2. Let n be the length of S. + var n = s.length; + + // 3. Initialize i to 0. + var i = 0; + + // 4. Initialize U to be an empty sequence of Unicode characters. + var u = []; + + // 5. While i < n: + while (i < n) { + + // 1. Let c be the code unit in S at index i. + var c = s.charCodeAt(i); + + // 2. Depending on the value of c: + + // c < 0xD800 or c > 0xDFFF + if (c < 0xD800 || c > 0xDFFF) { + // Append to U the Unicode character with code point c. + u.push(c); + } + + // 0xDC00 ≤ c ≤ 0xDFFF + else if (0xDC00 <= c && c <= 0xDFFF) { + // Append to U a U+FFFD REPLACEMENT CHARACTER. + u.push(0xFFFD); + } + + // 0xD800 ≤ c ≤ 0xDBFF + else if (0xD800 <= c && c <= 0xDBFF) { + // 1. If i = n−1, then append to U a U+FFFD REPLACEMENT + // CHARACTER. + if (i === n - 1) { + u.push(0xFFFD); + } + // 2. Otherwise, i < n−1: + else { + // 1. Let d be the code unit in S at index i+1. + var d = s.charCodeAt(i + 1); + + // 2. If 0xDC00 ≤ d ≤ 0xDFFF, then: + if (0xDC00 <= d && d <= 0xDFFF) { + // 1. Let a be c & 0x3FF. + var a = c & 0x3FF; + + // 2. Let b be d & 0x3FF. + var b = d & 0x3FF; + + // 3. Append to U the Unicode character with code point + // 2^16+2^10*a+b. + u.push(0x10000 + (a << 10) + b); + + // 4. Set i to i+1. + i += 1; + } + + // 3. Otherwise, d < 0xDC00 or d > 0xDFFF. Append to U a + // U+FFFD REPLACEMENT CHARACTER. + else { + u.push(0xFFFD); + } + } + } + + // 3. Set i to i+1. + i += 1; + } + + // 6. Return U. + return u; + } + + /** + * @param {!Array.<number>} code_points Array of code points. + * @return {string} string String of UTF-16 code units. + */ + function codePointsToString(code_points) { + var s = ''; + for (var i = 0; i < code_points.length; ++i) { + var cp = code_points[i]; + if (cp <= 0xFFFF) { + s += String.fromCharCode(cp); + } else { + cp -= 0x10000; + s += String.fromCharCode((cp >> 10) + 0xD800, + (cp & 0x3FF) + 0xDC00); + } + } + return s; + } + + + // + // Implementation of Encoding specification + // https://encoding.spec.whatwg.org/ + // + + // + // 4. Terminology + // + + /** + * An ASCII byte is a byte in the range 0x00 to 0x7F, inclusive. + * @param {number} a The number to test. + * @return {boolean} True if a is in the range 0x00 to 0x7F, inclusive. + */ + function isASCIIByte(a) { + return 0x00 <= a && a <= 0x7F; + } + + /** + * An ASCII code point is a code point in the range U+0000 to + * U+007F, inclusive. + */ + var isASCIICodePoint = isASCIIByte; + + + /** + * End-of-stream is a special token that signifies no more tokens + * are in the stream. + * @const + */ var end_of_stream = -1; + + /** + * A stream represents an ordered sequence of tokens. + * + * @constructor + * @param {!(Array.<number>|Uint8Array)} tokens Array of tokens that provide + * the stream. + */ + function Stream(tokens) { + /** @type {!Array.<number>} */ + this.tokens = [].slice.call(tokens); + // Reversed as push/pop is more efficient than shift/unshift. + this.tokens.reverse(); + } + + Stream.prototype = { + /** + * @return {boolean} True if end-of-stream has been hit. + */ + endOfStream: function() { + return !this.tokens.length; + }, + + /** + * When a token is read from a stream, the first token in the + * stream must be returned and subsequently removed, and + * end-of-stream must be returned otherwise. + * + * @return {number} Get the next token from the stream, or + * end_of_stream. + */ + read: function() { + if (!this.tokens.length) + return end_of_stream; + return this.tokens.pop(); + }, + + /** + * When one or more tokens are prepended to a stream, those tokens + * must be inserted, in given order, before the first token in the + * stream. + * + * @param {(number|!Array.<number>)} token The token(s) to prepend to the + * stream. + */ + prepend: function(token) { + if (Array.isArray(token)) { + var tokens = /**@type {!Array.<number>}*/(token); + while (tokens.length) + this.tokens.push(tokens.pop()); + } else { + this.tokens.push(token); + } + }, + + /** + * When one or more tokens are pushed to a stream, those tokens + * must be inserted, in given order, after the last token in the + * stream. + * + * @param {(number|!Array.<number>)} token The tokens(s) to push to the + * stream. + */ + push: function(token) { + if (Array.isArray(token)) { + var tokens = /**@type {!Array.<number>}*/(token); + while (tokens.length) + this.tokens.unshift(tokens.shift()); + } else { + this.tokens.unshift(token); + } + } + }; + + // + // 5. Encodings + // + + // 5.1 Encoders and decoders + + /** @const */ + var finished = -1; + + /** + * @param {boolean} fatal If true, decoding errors raise an exception. + * @param {number=} opt_code_point Override the standard fallback code point. + * @return {number} The code point to insert on a decoding error. + */ + function decoderError(fatal, opt_code_point) { + if (fatal) + throw TypeError('Decoder error'); + return opt_code_point || 0xFFFD; + } + + /** + * @param {number} code_point The code point that could not be encoded. + * @return {number} Always throws, no value is actually returned. + */ + function encoderError(code_point) { + throw TypeError('The code point ' + code_point + ' could not be encoded.'); + } + + /** @interface */ + function Decoder() {} + Decoder.prototype = { + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point, or |finished|. + */ + handler: function(stream, bite) {} + }; + + /** @interface */ + function Encoder() {} + Encoder.prototype = { + /** + * @param {Stream} stream The stream of code points being encoded. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit, or |finished|. + */ + handler: function(stream, code_point) {} + }; + + // 5.2 Names and labels + + // TODO: Define @typedef for Encoding: {name:string,labels:Array.<string>} + // https://github.com/google/closure-compiler/issues/247 + + /** + * @param {string} label The encoding label. + * @return {?{name:string,labels:Array.<string>}} + */ + function getEncoding(label) { + // 1. Remove any leading and trailing ASCII whitespace from label. + label = String(label).trim().toLowerCase(); + + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, return the corresponding + // encoding, and failure otherwise. + if (Object.prototype.hasOwnProperty.call(label_to_encoding, label)) { + return label_to_encoding[label]; + } + return null; + } + + /** + * Encodings table: https://encoding.spec.whatwg.org/encodings.json + * @const + * @type {!Array.<{ + * heading: string, + * encodings: Array.<{name:string,labels:Array.<string>}> + * }>} + */ + var encodings = [ + { + "encodings": [ + { + "labels": [ + "unicode-1-1-utf-8", + "utf-8", + "utf8" + ], + "name": "UTF-8" + } + ], + "heading": "The Encoding" + }, + { + "encodings": [ + { + "labels": [ + "866", + "cp866", + "csibm866", + "ibm866" + ], + "name": "IBM866" + }, + { + "labels": [ + "csisolatin2", + "iso-8859-2", + "iso-ir-101", + "iso8859-2", + "iso88592", + "iso_8859-2", + "iso_8859-2:1987", + "l2", + "latin2" + ], + "name": "ISO-8859-2" + }, + { + "labels": [ + "csisolatin3", + "iso-8859-3", + "iso-ir-109", + "iso8859-3", + "iso88593", + "iso_8859-3", + "iso_8859-3:1988", + "l3", + "latin3" + ], + "name": "ISO-8859-3" + }, + { + "labels": [ + "csisolatin4", + "iso-8859-4", + "iso-ir-110", + "iso8859-4", + "iso88594", + "iso_8859-4", + "iso_8859-4:1988", + "l4", + "latin4" + ], + "name": "ISO-8859-4" + }, + { + "labels": [ + "csisolatincyrillic", + "cyrillic", + "iso-8859-5", + "iso-ir-144", + "iso8859-5", + "iso88595", + "iso_8859-5", + "iso_8859-5:1988" + ], + "name": "ISO-8859-5" + }, + { + "labels": [ + "arabic", + "asmo-708", + "csiso88596e", + "csiso88596i", + "csisolatinarabic", + "ecma-114", + "iso-8859-6", + "iso-8859-6-e", + "iso-8859-6-i", + "iso-ir-127", + "iso8859-6", + "iso88596", + "iso_8859-6", + "iso_8859-6:1987" + ], + "name": "ISO-8859-6" + }, + { + "labels": [ + "csisolatingreek", + "ecma-118", + "elot_928", + "greek", + "greek8", + "iso-8859-7", + "iso-ir-126", + "iso8859-7", + "iso88597", + "iso_8859-7", + "iso_8859-7:1987", + "sun_eu_greek" + ], + "name": "ISO-8859-7" + }, + { + "labels": [ + "csiso88598e", + "csisolatinhebrew", + "hebrew", + "iso-8859-8", + "iso-8859-8-e", + "iso-ir-138", + "iso8859-8", + "iso88598", + "iso_8859-8", + "iso_8859-8:1988", + "visual" + ], + "name": "ISO-8859-8" + }, + { + "labels": [ + "csiso88598i", + "iso-8859-8-i", + "logical" + ], + "name": "ISO-8859-8-I" + }, + { + "labels": [ + "csisolatin6", + "iso-8859-10", + "iso-ir-157", + "iso8859-10", + "iso885910", + "l6", + "latin6" + ], + "name": "ISO-8859-10" + }, + { + "labels": [ + "iso-8859-13", + "iso8859-13", + "iso885913" + ], + "name": "ISO-8859-13" + }, + { + "labels": [ + "iso-8859-14", + "iso8859-14", + "iso885914" + ], + "name": "ISO-8859-14" + }, + { + "labels": [ + "csisolatin9", + "iso-8859-15", + "iso8859-15", + "iso885915", + "iso_8859-15", + "l9" + ], + "name": "ISO-8859-15" + }, + { + "labels": [ + "iso-8859-16" + ], + "name": "ISO-8859-16" + }, + { + "labels": [ + "cskoi8r", + "koi", + "koi8", + "koi8-r", + "koi8_r" + ], + "name": "KOI8-R" + }, + { + "labels": [ + "koi8-ru", + "koi8-u" + ], + "name": "KOI8-U" + }, + { + "labels": [ + "csmacintosh", + "mac", + "macintosh", + "x-mac-roman" + ], + "name": "macintosh" + }, + { + "labels": [ + "dos-874", + "iso-8859-11", + "iso8859-11", + "iso885911", + "tis-620", + "windows-874" + ], + "name": "windows-874" + }, + { + "labels": [ + "cp1250", + "windows-1250", + "x-cp1250" + ], + "name": "windows-1250" + }, + { + "labels": [ + "cp1251", + "windows-1251", + "x-cp1251" + ], + "name": "windows-1251" + }, + { + "labels": [ + "ansi_x3.4-1968", + "ascii", + "cp1252", + "cp819", + "csisolatin1", + "ibm819", + "iso-8859-1", + "iso-ir-100", + "iso8859-1", + "iso88591", + "iso_8859-1", + "iso_8859-1:1987", + "l1", + "latin1", + "us-ascii", + "windows-1252", + "x-cp1252" + ], + "name": "windows-1252" + }, + { + "labels": [ + "cp1253", + "windows-1253", + "x-cp1253" + ], + "name": "windows-1253" + }, + { + "labels": [ + "cp1254", + "csisolatin5", + "iso-8859-9", + "iso-ir-148", + "iso8859-9", + "iso88599", + "iso_8859-9", + "iso_8859-9:1989", + "l5", + "latin5", + "windows-1254", + "x-cp1254" + ], + "name": "windows-1254" + }, + { + "labels": [ + "cp1255", + "windows-1255", + "x-cp1255" + ], + "name": "windows-1255" + }, + { + "labels": [ + "cp1256", + "windows-1256", + "x-cp1256" + ], + "name": "windows-1256" + }, + { + "labels": [ + "cp1257", + "windows-1257", + "x-cp1257" + ], + "name": "windows-1257" + }, + { + "labels": [ + "cp1258", + "windows-1258", + "x-cp1258" + ], + "name": "windows-1258" + }, + { + "labels": [ + "x-mac-cyrillic", + "x-mac-ukrainian" + ], + "name": "x-mac-cyrillic" + } + ], + "heading": "Legacy single-byte encodings" + }, + { + "encodings": [ + { + "labels": [ + "chinese", + "csgb2312", + "csiso58gb231280", + "gb2312", + "gb_2312", + "gb_2312-80", + "gbk", + "iso-ir-58", + "x-gbk" + ], + "name": "GBK" + }, + { + "labels": [ + "gb18030" + ], + "name": "gb18030" + } + ], + "heading": "Legacy multi-byte Chinese (simplified) encodings" + }, + { + "encodings": [ + { + "labels": [ + "big5", + "big5-hkscs", + "cn-big5", + "csbig5", + "x-x-big5" + ], + "name": "Big5" + } + ], + "heading": "Legacy multi-byte Chinese (traditional) encodings" + }, + { + "encodings": [ + { + "labels": [ + "cseucpkdfmtjapanese", + "euc-jp", + "x-euc-jp" + ], + "name": "EUC-JP" + }, + { + "labels": [ + "csiso2022jp", + "iso-2022-jp" + ], + "name": "ISO-2022-JP" + }, + { + "labels": [ + "csshiftjis", + "ms932", + "ms_kanji", + "shift-jis", + "shift_jis", + "sjis", + "windows-31j", + "x-sjis" + ], + "name": "Shift_JIS" + } + ], + "heading": "Legacy multi-byte Japanese encodings" + }, + { + "encodings": [ + { + "labels": [ + "cseuckr", + "csksc56011987", + "euc-kr", + "iso-ir-149", + "korean", + "ks_c_5601-1987", + "ks_c_5601-1989", + "ksc5601", + "ksc_5601", + "windows-949" + ], + "name": "EUC-KR" + } + ], + "heading": "Legacy multi-byte Korean encodings" + }, + { + "encodings": [ + { + "labels": [ + "csiso2022kr", + "hz-gb-2312", + "iso-2022-cn", + "iso-2022-cn-ext", + "iso-2022-kr" + ], + "name": "replacement" + }, + { + "labels": [ + "utf-16be" + ], + "name": "UTF-16BE" + }, + { + "labels": [ + "utf-16", + "utf-16le" + ], + "name": "UTF-16LE" + }, + { + "labels": [ + "x-user-defined" + ], + "name": "x-user-defined" + } + ], + "heading": "Legacy miscellaneous encodings" + } + ]; + + // Label to encoding registry. + /** @type {Object.<string,{name:string,labels:Array.<string>}>} */ + var label_to_encoding = {}; + encodings.forEach(function(category) { + category.encodings.forEach(function(encoding) { + encoding.labels.forEach(function(label) { + label_to_encoding[label] = encoding; + }); + }); + }); + + // Registry of of encoder/decoder factories, by encoding name. + /** @type {Object.<string, function({fatal:boolean}): Encoder>} */ + var encoders = {}; + /** @type {Object.<string, function({fatal:boolean}): Decoder>} */ + var decoders = {}; + + // + // 6. Indexes + // + + /** + * @param {number} pointer The |pointer| to search for. + * @param {(!Array.<?number>|undefined)} index The |index| to search within. + * @return {?number} The code point corresponding to |pointer| in |index|, + * or null if |code point| is not in |index|. + */ + function indexCodePointFor(pointer, index) { + if (!index) return null; + return index[pointer] || null; + } + + /** + * @param {number} code_point The |code point| to search for. + * @param {!Array.<?number>} index The |index| to search within. + * @return {?number} The first pointer corresponding to |code point| in + * |index|, or null if |code point| is not in |index|. + */ + function indexPointerFor(code_point, index) { + var pointer = index.indexOf(code_point); + return pointer === -1 ? null : pointer; + } + + /** + * @param {string} name Name of the index. + * @return {(!Array.<number>|!Array.<Array.<number>>)} + * */ + function index(name) { + if (!('encoding-indexes' in global)) { + throw Error("Indexes missing." + + " Did you forget to include encoding-indexes.js first?"); + } + return global['encoding-indexes'][name]; + } + + /** + * @param {number} pointer The |pointer| to search for in the gb18030 index. + * @return {?number} The code point corresponding to |pointer| in |index|, + * or null if |code point| is not in the gb18030 index. + */ + function indexGB18030RangesCodePointFor(pointer) { + // 1. If pointer is greater than 39419 and less than 189000, or + // pointer is greater than 1237575, return null. + if ((pointer > 39419 && pointer < 189000) || (pointer > 1237575)) + return null; + + // 2. If pointer is 7457, return code point U+E7C7. + if (pointer === 7457) return 0xE7C7; + + // 3. Let offset be the last pointer in index gb18030 ranges that + // is equal to or less than pointer and let code point offset be + // its corresponding code point. + var offset = 0; + var code_point_offset = 0; + var idx = index('gb18030-ranges'); + var i; + for (i = 0; i < idx.length; ++i) { + /** @type {!Array.<number>} */ + var entry = idx[i]; + if (entry[0] <= pointer) { + offset = entry[0]; + code_point_offset = entry[1]; + } else { + break; + } + } + + // 4. Return a code point whose value is code point offset + + // pointer − offset. + return code_point_offset + pointer - offset; + } + + /** + * @param {number} code_point The |code point| to locate in the gb18030 index. + * @return {number} The first pointer corresponding to |code point| in the + * gb18030 index. + */ + function indexGB18030RangesPointerFor(code_point) { + // 1. If code point is U+E7C7, return pointer 7457. + if (code_point === 0xE7C7) return 7457; + + // 2. Let offset be the last code point in index gb18030 ranges + // that is equal to or less than code point and let pointer offset + // be its corresponding pointer. + var offset = 0; + var pointer_offset = 0; + var idx = index('gb18030-ranges'); + var i; + for (i = 0; i < idx.length; ++i) { + /** @type {!Array.<number>} */ + var entry = idx[i]; + if (entry[1] <= code_point) { + offset = entry[1]; + pointer_offset = entry[0]; + } else { + break; + } + } + + // 3. Return a pointer whose value is pointer offset + code point + // − offset. + return pointer_offset + code_point - offset; + } + + /** + * @param {number} code_point The |code_point| to search for in the Shift_JIS + * index. + * @return {?number} The code point corresponding to |pointer| in |index|, + * or null if |code point| is not in the Shift_JIS index. + */ + function indexShiftJISPointerFor(code_point) { + // 1. Let index be index jis0208 excluding all entries whose + // pointer is in the range 8272 to 8835, inclusive. + shift_jis_index = shift_jis_index || + index('jis0208').map(function(code_point, pointer) { + return inRange(pointer, 8272, 8835) ? null : code_point; + }); + var index_ = shift_jis_index; + + // 2. Return the index pointer for code point in index. + return index_.indexOf(code_point); + } + var shift_jis_index; + + /** + * @param {number} code_point The |code_point| to search for in the big5 + * index. + * @return {?number} The code point corresponding to |pointer| in |index|, + * or null if |code point| is not in the big5 index. + */ + function indexBig5PointerFor(code_point) { + // 1. Let index be index Big5 excluding all entries whose pointer + big5_index_no_hkscs = big5_index_no_hkscs || + index('big5').map(function(code_point, pointer) { + return (pointer < (0xA1 - 0x81) * 157) ? null : code_point; + }); + var index_ = big5_index_no_hkscs; + + // 2. If code point is U+2550, U+255E, U+2561, U+256A, U+5341, or + // U+5345, return the last pointer corresponding to code point in + // index. + if (code_point === 0x2550 || code_point === 0x255E || + code_point === 0x2561 || code_point === 0x256A || + code_point === 0x5341 || code_point === 0x5345) { + return index_.lastIndexOf(code_point); + } + + // 3. Return the index pointer for code point in index. + return indexPointerFor(code_point, index_); + } + var big5_index_no_hkscs; + + // + // 8. API + // + + /** @const */ var DEFAULT_ENCODING = 'utf-8'; + + // 8.1 Interface TextDecoder + + /** + * @constructor + * @param {string=} label The label of the encoding; + * defaults to 'utf-8'. + * @param {Object=} options + */ + function TextDecoder(label, options) { + // Web IDL conventions + if (!(this instanceof TextDecoder)) + throw TypeError('Called as a function. Did you forget \'new\'?'); + label = label !== undefined ? String(label) : DEFAULT_ENCODING; + options = ToDictionary(options); + + // A TextDecoder object has an associated encoding, decoder, + // stream, ignore BOM flag (initially unset), BOM seen flag + // (initially unset), error mode (initially replacement), and do + // not flush flag (initially unset). + + /** @private */ + this._encoding = null; + /** @private @type {?Decoder} */ + this._decoder = null; + /** @private @type {boolean} */ + this._ignoreBOM = false; + /** @private @type {boolean} */ + this._BOMseen = false; + /** @private @type {string} */ + this._error_mode = 'replacement'; + /** @private @type {boolean} */ + this._do_not_flush = false; + + + // 1. Let encoding be the result of getting an encoding from + // label. + var encoding = getEncoding(label); + + // 2. If encoding is failure or replacement, throw a RangeError. + if (encoding === null || encoding.name === 'replacement') + throw RangeError('Unknown encoding: ' + label); + if (!decoders[encoding.name]) { + throw Error('Decoder not present.' + + ' Did you forget to include encoding-indexes.js first?'); + } + + // 3. Let dec be a new TextDecoder object. + var dec = this; + + // 4. Set dec's encoding to encoding. + dec._encoding = encoding; + + // 5. If options's fatal member is true, set dec's error mode to + // fatal. + if (Boolean(options['fatal'])) + dec._error_mode = 'fatal'; + + // 6. If options's ignoreBOM member is true, set dec's ignore BOM + // flag. + if (Boolean(options['ignoreBOM'])) + dec._ignoreBOM = true; + + // For pre-ES5 runtimes: + if (!Object.defineProperty) { + this.encoding = dec._encoding.name.toLowerCase(); + this.fatal = dec._error_mode === 'fatal'; + this.ignoreBOM = dec._ignoreBOM; + } + + // 7. Return dec. + return dec; + } + + if (Object.defineProperty) { + // The encoding attribute's getter must return encoding's name. + Object.defineProperty(TextDecoder.prototype, 'encoding', { + /** @this {TextDecoder} */ + get: function() { return this._encoding.name.toLowerCase(); } + }); + + // The fatal attribute's getter must return true if error mode + // is fatal, and false otherwise. + Object.defineProperty(TextDecoder.prototype, 'fatal', { + /** @this {TextDecoder} */ + get: function() { return this._error_mode === 'fatal'; } + }); + + // The ignoreBOM attribute's getter must return true if ignore + // BOM flag is set, and false otherwise. + Object.defineProperty(TextDecoder.prototype, 'ignoreBOM', { + /** @this {TextDecoder} */ + get: function() { return this._ignoreBOM; } + }); + } + + /** + * @param {BufferSource=} input The buffer of bytes to decode. + * @param {Object=} options + * @return {string} The decoded string. + */ + TextDecoder.prototype.decode = function decode(input, options) { + var bytes; + if (typeof input === 'object' && input instanceof ArrayBuffer) { + bytes = new Uint8Array(input); + } else if (typeof input === 'object' && 'buffer' in input && + input.buffer instanceof ArrayBuffer) { + bytes = new Uint8Array(input.buffer, + input.byteOffset, + input.byteLength); + } else { + bytes = new Uint8Array(0); + } + + options = ToDictionary(options); + + // 1. If the do not flush flag is unset, set decoder to a new + // encoding's decoder, set stream to a new stream, and unset the + // BOM seen flag. + if (!this._do_not_flush) { + this._decoder = decoders[this._encoding.name]({ + fatal: this._error_mode === 'fatal'}); + this._BOMseen = false; + } + + // 2. If options's stream is true, set the do not flush flag, and + // unset the do not flush flag otherwise. + this._do_not_flush = Boolean(options['stream']); + + // 3. If input is given, push a copy of input to stream. + // TODO: Align with spec algorithm - maintain stream on instance. + var input_stream = new Stream(bytes); + + // 4. Let output be a new stream. + var output = []; + + /** @type {?(number|!Array.<number>)} */ + var result; + + // 5. While true: + while (true) { + // 1. Let token be the result of reading from stream. + var token = input_stream.read(); + + // 2. If token is end-of-stream and the do not flush flag is + // set, return output, serialized. + // TODO: Align with spec algorithm. + if (token === end_of_stream) + break; + + // 3. Otherwise, run these subsubsteps: + + // 1. Let result be the result of processing token for decoder, + // stream, output, and error mode. + result = this._decoder.handler(input_stream, token); + + // 2. If result is finished, return output, serialized. + if (result === finished) + break; + + if (result !== null) { + if (Array.isArray(result)) + output.push.apply(output, /**@type {!Array.<number>}*/(result)); + else + output.push(result); + } + + // 3. Otherwise, if result is error, throw a TypeError. + // (Thrown in handler) + + // 4. Otherwise, do nothing. + } + // TODO: Align with spec algorithm. + if (!this._do_not_flush) { + do { + result = this._decoder.handler(input_stream, input_stream.read()); + if (result === finished) + break; + if (result === null) + continue; + if (Array.isArray(result)) + output.push.apply(output, /**@type {!Array.<number>}*/(result)); + else + output.push(result); + } while (!input_stream.endOfStream()); + this._decoder = null; + } + + // A TextDecoder object also has an associated serialize stream + // algorithm... + /** + * @param {!Array.<number>} stream + * @return {string} + * @this {TextDecoder} + */ + function serializeStream(stream) { + // 1. Let token be the result of reading from stream. + // (Done in-place on array, rather than as a stream) + + // 2. If encoding is UTF-8, UTF-16BE, or UTF-16LE, and ignore + // BOM flag and BOM seen flag are unset, run these subsubsteps: + if (includes(['UTF-8', 'UTF-16LE', 'UTF-16BE'], this._encoding.name) && + !this._ignoreBOM && !this._BOMseen) { + if (stream.length > 0 && stream[0] === 0xFEFF) { + // 1. If token is U+FEFF, set BOM seen flag. + this._BOMseen = true; + stream.shift(); + } else if (stream.length > 0) { + // 2. Otherwise, if token is not end-of-stream, set BOM seen + // flag and append token to stream. + this._BOMseen = true; + } else { + // 3. Otherwise, if token is not end-of-stream, append token + // to output. + // (no-op) + } + } + // 4. Otherwise, return output. + return codePointsToString(stream); + } + + return serializeStream.call(this, output); + }; + + // 8.2 Interface TextEncoder + + /** + * @constructor + * @param {string=} label The label of the encoding. NONSTANDARD. + * @param {Object=} options NONSTANDARD. + */ + function TextEncoder(label, options) { + // Web IDL conventions + if (!(this instanceof TextEncoder)) + throw TypeError('Called as a function. Did you forget \'new\'?'); + options = ToDictionary(options); + + // A TextEncoder object has an associated encoding and encoder. + + /** @private */ + this._encoding = null; + /** @private @type {?Encoder} */ + this._encoder = null; + + // Non-standard + /** @private @type {boolean} */ + this._do_not_flush = false; + /** @private @type {string} */ + this._fatal = Boolean(options['fatal']) ? 'fatal' : 'replacement'; + + // 1. Let enc be a new TextEncoder object. + var enc = this; + + // 2. Set enc's encoding to UTF-8's encoder. + if (Boolean(options['NONSTANDARD_allowLegacyEncoding'])) { + // NONSTANDARD behavior. + label = label !== undefined ? String(label) : DEFAULT_ENCODING; + var encoding = getEncoding(label); + if (encoding === null || encoding.name === 'replacement') + throw RangeError('Unknown encoding: ' + label); + if (!encoders[encoding.name]) { + throw Error('Encoder not present.' + + ' Did you forget to include encoding-indexes.js first?'); + } + enc._encoding = encoding; + } else { + // Standard behavior. + enc._encoding = getEncoding('utf-8'); + + if (label !== undefined && 'console' in global) { + console.warn('TextEncoder constructor called with encoding label, ' + + 'which is ignored.'); + } + } + + // For pre-ES5 runtimes: + if (!Object.defineProperty) + this.encoding = enc._encoding.name.toLowerCase(); + + // 3. Return enc. + return enc; + } + + if (Object.defineProperty) { + // The encoding attribute's getter must return encoding's name. + Object.defineProperty(TextEncoder.prototype, 'encoding', { + /** @this {TextEncoder} */ + get: function() { return this._encoding.name.toLowerCase(); } + }); + } + + /** + * @param {string=} opt_string The string to encode. + * @param {Object=} options + * @return {!Uint8Array} Encoded bytes, as a Uint8Array. + */ + TextEncoder.prototype.encode = function encode(opt_string, options) { + opt_string = opt_string === undefined ? '' : String(opt_string); + options = ToDictionary(options); + + // NOTE: This option is nonstandard. None of the encodings + // permitted for encoding (i.e. UTF-8, UTF-16) are stateful when + // the input is a USVString so streaming is not necessary. + if (!this._do_not_flush) + this._encoder = encoders[this._encoding.name]({ + fatal: this._fatal === 'fatal'}); + this._do_not_flush = Boolean(options['stream']); + + // 1. Convert input to a stream. + var input = new Stream(stringToCodePoints(opt_string)); + + // 2. Let output be a new stream + var output = []; + + /** @type {?(number|!Array.<number>)} */ + var result; + // 3. While true, run these substeps: + while (true) { + // 1. Let token be the result of reading from input. + var token = input.read(); + if (token === end_of_stream) + break; + // 2. Let result be the result of processing token for encoder, + // input, output. + result = this._encoder.handler(input, token); + if (result === finished) + break; + if (Array.isArray(result)) + output.push.apply(output, /**@type {!Array.<number>}*/(result)); + else + output.push(result); + } + // TODO: Align with spec algorithm. + if (!this._do_not_flush) { + while (true) { + result = this._encoder.handler(input, input.read()); + if (result === finished) + break; + if (Array.isArray(result)) + output.push.apply(output, /**@type {!Array.<number>}*/(result)); + else + output.push(result); + } + this._encoder = null; + } + // 3. If result is finished, convert output into a byte sequence, + // and then return a Uint8Array object wrapping an ArrayBuffer + // containing output. + return new Uint8Array(output); + }; + + + // + // 9. The encoding + // + + // 9.1 utf-8 + + // 9.1.1 utf-8 decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function UTF8Decoder(options) { + var fatal = options.fatal; + + // utf-8's decoder's has an associated utf-8 code point, utf-8 + // bytes seen, and utf-8 bytes needed (all initially 0), a utf-8 + // lower boundary (initially 0x80), and a utf-8 upper boundary + // (initially 0xBF). + var /** @type {number} */ utf8_code_point = 0, + /** @type {number} */ utf8_bytes_seen = 0, + /** @type {number} */ utf8_bytes_needed = 0, + /** @type {number} */ utf8_lower_boundary = 0x80, + /** @type {number} */ utf8_upper_boundary = 0xBF; + + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and utf-8 bytes needed is not 0, + // set utf-8 bytes needed to 0 and return error. + if (bite === end_of_stream && utf8_bytes_needed !== 0) { + utf8_bytes_needed = 0; + return decoderError(fatal); + } + + // 2. If byte is end-of-stream, return finished. + if (bite === end_of_stream) + return finished; + + // 3. If utf-8 bytes needed is 0, based on byte: + if (utf8_bytes_needed === 0) { + + // 0x00 to 0x7F + if (inRange(bite, 0x00, 0x7F)) { + // Return a code point whose value is byte. + return bite; + } + + // 0xC2 to 0xDF + else if (inRange(bite, 0xC2, 0xDF)) { + // 1. Set utf-8 bytes needed to 1. + utf8_bytes_needed = 1; + + // 2. Set UTF-8 code point to byte & 0x1F. + utf8_code_point = bite & 0x1F; + } + + // 0xE0 to 0xEF + else if (inRange(bite, 0xE0, 0xEF)) { + // 1. If byte is 0xE0, set utf-8 lower boundary to 0xA0. + if (bite === 0xE0) + utf8_lower_boundary = 0xA0; + // 2. If byte is 0xED, set utf-8 upper boundary to 0x9F. + if (bite === 0xED) + utf8_upper_boundary = 0x9F; + // 3. Set utf-8 bytes needed to 2. + utf8_bytes_needed = 2; + // 4. Set UTF-8 code point to byte & 0xF. + utf8_code_point = bite & 0xF; + } + + // 0xF0 to 0xF4 + else if (inRange(bite, 0xF0, 0xF4)) { + // 1. If byte is 0xF0, set utf-8 lower boundary to 0x90. + if (bite === 0xF0) + utf8_lower_boundary = 0x90; + // 2. If byte is 0xF4, set utf-8 upper boundary to 0x8F. + if (bite === 0xF4) + utf8_upper_boundary = 0x8F; + // 3. Set utf-8 bytes needed to 3. + utf8_bytes_needed = 3; + // 4. Set UTF-8 code point to byte & 0x7. + utf8_code_point = bite & 0x7; + } + + // Otherwise + else { + // Return error. + return decoderError(fatal); + } + + // Return continue. + return null; + } + + // 4. If byte is not in the range utf-8 lower boundary to utf-8 + // upper boundary, inclusive, run these substeps: + if (!inRange(bite, utf8_lower_boundary, utf8_upper_boundary)) { + + // 1. Set utf-8 code point, utf-8 bytes needed, and utf-8 + // bytes seen to 0, set utf-8 lower boundary to 0x80, and set + // utf-8 upper boundary to 0xBF. + utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0; + utf8_lower_boundary = 0x80; + utf8_upper_boundary = 0xBF; + + // 2. Prepend byte to stream. + stream.prepend(bite); + + // 3. Return error. + return decoderError(fatal); + } + + // 5. Set utf-8 lower boundary to 0x80 and utf-8 upper boundary + // to 0xBF. + utf8_lower_boundary = 0x80; + utf8_upper_boundary = 0xBF; + + // 6. Set UTF-8 code point to (UTF-8 code point << 6) | (byte & + // 0x3F) + utf8_code_point = (utf8_code_point << 6) | (bite & 0x3F); + + // 7. Increase utf-8 bytes seen by one. + utf8_bytes_seen += 1; + + // 8. If utf-8 bytes seen is not equal to utf-8 bytes needed, + // continue. + if (utf8_bytes_seen !== utf8_bytes_needed) + return null; + + // 9. Let code point be utf-8 code point. + var code_point = utf8_code_point; + + // 10. Set utf-8 code point, utf-8 bytes needed, and utf-8 bytes + // seen to 0. + utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0; + + // 11. Return a code point whose value is code point. + return code_point; + }; + } + + // 9.1.2 utf-8 encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function UTF8Encoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. Set count and offset based on the range code point is in: + var count, offset; + // U+0080 to U+07FF, inclusive: + if (inRange(code_point, 0x0080, 0x07FF)) { + // 1 and 0xC0 + count = 1; + offset = 0xC0; + } + // U+0800 to U+FFFF, inclusive: + else if (inRange(code_point, 0x0800, 0xFFFF)) { + // 2 and 0xE0 + count = 2; + offset = 0xE0; + } + // U+10000 to U+10FFFF, inclusive: + else if (inRange(code_point, 0x10000, 0x10FFFF)) { + // 3 and 0xF0 + count = 3; + offset = 0xF0; + } + + // 4. Let bytes be a byte sequence whose first byte is (code + // point >> (6 × count)) + offset. + var bytes = [(code_point >> (6 * count)) + offset]; + + // 5. Run these substeps while count is greater than 0: + while (count > 0) { + + // 1. Set temp to code point >> (6 × (count − 1)). + var temp = code_point >> (6 * (count - 1)); + + // 2. Append to bytes 0x80 | (temp & 0x3F). + bytes.push(0x80 | (temp & 0x3F)); + + // 3. Decrease count by one. + count -= 1; + } + + // 6. Return bytes bytes, in order. + return bytes; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['UTF-8'] = function(options) { + return new UTF8Encoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['UTF-8'] = function(options) { + return new UTF8Decoder(options); + }; + + // + // 10. Legacy single-byte encodings + // + + // 10.1 single-byte decoder + /** + * @constructor + * @implements {Decoder} + * @param {!Array.<number>} index The encoding index. + * @param {{fatal: boolean}} options + */ + function SingleByteDecoder(index, options) { + var fatal = options.fatal; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream, return finished. + if (bite === end_of_stream) + return finished; + + // 2. If byte is an ASCII byte, return a code point whose value + // is byte. + if (isASCIIByte(bite)) + return bite; + + // 3. Let code point be the index code point for byte − 0x80 in + // index single-byte. + var code_point = index[bite - 0x80]; + + // 4. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 5. Return a code point whose value is code point. + return code_point; + }; + } + + // 10.2 single-byte encoder + /** + * @constructor + * @implements {Encoder} + * @param {!Array.<?number>} index The encoding index. + * @param {{fatal: boolean}} options + */ + function SingleByteEncoder(index, options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. Let pointer be the index pointer for code point in index + // single-byte. + var pointer = indexPointerFor(code_point, index); + + // 4. If pointer is null, return error with code point. + if (pointer === null) + encoderError(code_point); + + // 5. Return a byte whose value is pointer + 0x80. + return pointer + 0x80; + }; + } + + (function() { + if (!('encoding-indexes' in global)) + return; + encodings.forEach(function(category) { + if (category.heading !== 'Legacy single-byte encodings') + return; + category.encodings.forEach(function(encoding) { + var name = encoding.name; + var idx = index(name.toLowerCase()); + /** @param {{fatal: boolean}} options */ + decoders[name] = function(options) { + return new SingleByteDecoder(idx, options); + }; + /** @param {{fatal: boolean}} options */ + encoders[name] = function(options) { + return new SingleByteEncoder(idx, options); + }; + }); + }); + }()); + + // + // 11. Legacy multi-byte Chinese (simplified) encodings + // + + // 11.1 gbk + + // 11.1.1 gbk decoder + // gbk's decoder is gb18030's decoder. + /** @param {{fatal: boolean}} options */ + decoders['GBK'] = function(options) { + return new GB18030Decoder(options); + }; + + // 11.1.2 gbk encoder + // gbk's encoder is gb18030's encoder with its gbk flag set. + /** @param {{fatal: boolean}} options */ + encoders['GBK'] = function(options) { + return new GB18030Encoder(options, true); + }; + + // 11.2 gb18030 + + // 11.2.1 gb18030 decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function GB18030Decoder(options) { + var fatal = options.fatal; + // gb18030's decoder has an associated gb18030 first, gb18030 + // second, and gb18030 third (all initially 0x00). + var /** @type {number} */ gb18030_first = 0x00, + /** @type {number} */ gb18030_second = 0x00, + /** @type {number} */ gb18030_third = 0x00; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and gb18030 first, gb18030 + // second, and gb18030 third are 0x00, return finished. + if (bite === end_of_stream && gb18030_first === 0x00 && + gb18030_second === 0x00 && gb18030_third === 0x00) { + return finished; + } + // 2. If byte is end-of-stream, and gb18030 first, gb18030 + // second, or gb18030 third is not 0x00, set gb18030 first, + // gb18030 second, and gb18030 third to 0x00, and return error. + if (bite === end_of_stream && + (gb18030_first !== 0x00 || gb18030_second !== 0x00 || + gb18030_third !== 0x00)) { + gb18030_first = 0x00; + gb18030_second = 0x00; + gb18030_third = 0x00; + decoderError(fatal); + } + var code_point; + // 3. If gb18030 third is not 0x00, run these substeps: + if (gb18030_third !== 0x00) { + // 1. Let code point be null. + code_point = null; + // 2. If byte is in the range 0x30 to 0x39, inclusive, set + // code point to the index gb18030 ranges code point for + // (((gb18030 first − 0x81) × 10 + gb18030 second − 0x30) × + // 126 + gb18030 third − 0x81) × 10 + byte − 0x30. + if (inRange(bite, 0x30, 0x39)) { + code_point = indexGB18030RangesCodePointFor( + (((gb18030_first - 0x81) * 10 + gb18030_second - 0x30) * 126 + + gb18030_third - 0x81) * 10 + bite - 0x30); + } + + // 3. Let buffer be a byte sequence consisting of gb18030 + // second, gb18030 third, and byte, in order. + var buffer = [gb18030_second, gb18030_third, bite]; + + // 4. Set gb18030 first, gb18030 second, and gb18030 third to + // 0x00. + gb18030_first = 0x00; + gb18030_second = 0x00; + gb18030_third = 0x00; + + // 5. If code point is null, prepend buffer to stream and + // return error. + if (code_point === null) { + stream.prepend(buffer); + return decoderError(fatal); + } + + // 6. Return a code point whose value is code point. + return code_point; + } + + // 4. If gb18030 second is not 0x00, run these substeps: + if (gb18030_second !== 0x00) { + + // 1. If byte is in the range 0x81 to 0xFE, inclusive, set + // gb18030 third to byte and return continue. + if (inRange(bite, 0x81, 0xFE)) { + gb18030_third = bite; + return null; + } + + // 2. Prepend gb18030 second followed by byte to stream, set + // gb18030 first and gb18030 second to 0x00, and return error. + stream.prepend([gb18030_second, bite]); + gb18030_first = 0x00; + gb18030_second = 0x00; + return decoderError(fatal); + } + + // 5. If gb18030 first is not 0x00, run these substeps: + if (gb18030_first !== 0x00) { + + // 1. If byte is in the range 0x30 to 0x39, inclusive, set + // gb18030 second to byte and return continue. + if (inRange(bite, 0x30, 0x39)) { + gb18030_second = bite; + return null; + } + + // 2. Let lead be gb18030 first, let pointer be null, and set + // gb18030 first to 0x00. + var lead = gb18030_first; + var pointer = null; + gb18030_first = 0x00; + + // 3. Let offset be 0x40 if byte is less than 0x7F and 0x41 + // otherwise. + var offset = bite < 0x7F ? 0x40 : 0x41; + + // 4. If byte is in the range 0x40 to 0x7E, inclusive, or 0x80 + // to 0xFE, inclusive, set pointer to (lead − 0x81) × 190 + + // (byte − offset). + if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFE)) + pointer = (lead - 0x81) * 190 + (bite - offset); + + // 5. Let code point be null if pointer is null and the index + // code point for pointer in index gb18030 otherwise. + code_point = pointer === null ? null : + indexCodePointFor(pointer, index('gb18030')); + + // 6. If code point is null and byte is an ASCII byte, prepend + // byte to stream. + if (code_point === null && isASCIIByte(bite)) + stream.prepend(bite); + + // 7. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 8. Return a code point whose value is code point. + return code_point; + } + + // 6. If byte is an ASCII byte, return a code point whose value + // is byte. + if (isASCIIByte(bite)) + return bite; + + // 7. If byte is 0x80, return code point U+20AC. + if (bite === 0x80) + return 0x20AC; + + // 8. If byte is in the range 0x81 to 0xFE, inclusive, set + // gb18030 first to byte and return continue. + if (inRange(bite, 0x81, 0xFE)) { + gb18030_first = bite; + return null; + } + + // 9. Return error. + return decoderError(fatal); + }; + } + + // 11.2.2 gb18030 encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + * @param {boolean=} gbk_flag + */ + function GB18030Encoder(options, gbk_flag) { + var fatal = options.fatal; + // gb18030's decoder has an associated gbk flag (initially unset). + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. If code point is U+E5E5, return error with code point. + if (code_point === 0xE5E5) + return encoderError(code_point); + + // 4. If the gbk flag is set and code point is U+20AC, return + // byte 0x80. + if (gbk_flag && code_point === 0x20AC) + return 0x80; + + // 5. Let pointer be the index pointer for code point in index + // gb18030. + var pointer = indexPointerFor(code_point, index('gb18030')); + + // 6. If pointer is not null, run these substeps: + if (pointer !== null) { + + // 1. Let lead be floor(pointer / 190) + 0x81. + var lead = floor(pointer / 190) + 0x81; + + // 2. Let trail be pointer % 190. + var trail = pointer % 190; + + // 3. Let offset be 0x40 if trail is less than 0x3F and 0x41 otherwise. + var offset = trail < 0x3F ? 0x40 : 0x41; + + // 4. Return two bytes whose values are lead and trail + offset. + return [lead, trail + offset]; + } + + // 7. If gbk flag is set, return error with code point. + if (gbk_flag) + return encoderError(code_point); + + // 8. Set pointer to the index gb18030 ranges pointer for code + // point. + pointer = indexGB18030RangesPointerFor(code_point); + + // 9. Let byte1 be floor(pointer / 10 / 126 / 10). + var byte1 = floor(pointer / 10 / 126 / 10); + + // 10. Set pointer to pointer − byte1 × 10 × 126 × 10. + pointer = pointer - byte1 * 10 * 126 * 10; + + // 11. Let byte2 be floor(pointer / 10 / 126). + var byte2 = floor(pointer / 10 / 126); + + // 12. Set pointer to pointer − byte2 × 10 × 126. + pointer = pointer - byte2 * 10 * 126; + + // 13. Let byte3 be floor(pointer / 10). + var byte3 = floor(pointer / 10); + + // 14. Let byte4 be pointer − byte3 × 10. + var byte4 = pointer - byte3 * 10; + + // 15. Return four bytes whose values are byte1 + 0x81, byte2 + + // 0x30, byte3 + 0x81, byte4 + 0x30. + return [byte1 + 0x81, + byte2 + 0x30, + byte3 + 0x81, + byte4 + 0x30]; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['gb18030'] = function(options) { + return new GB18030Encoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['gb18030'] = function(options) { + return new GB18030Decoder(options); + }; + + + // + // 12. Legacy multi-byte Chinese (traditional) encodings + // + + // 12.1 Big5 + + // 12.1.1 Big5 decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function Big5Decoder(options) { + var fatal = options.fatal; + // Big5's decoder has an associated Big5 lead (initially 0x00). + var /** @type {number} */ Big5_lead = 0x00; + + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and Big5 lead is not 0x00, set + // Big5 lead to 0x00 and return error. + if (bite === end_of_stream && Big5_lead !== 0x00) { + Big5_lead = 0x00; + return decoderError(fatal); + } + + // 2. If byte is end-of-stream and Big5 lead is 0x00, return + // finished. + if (bite === end_of_stream && Big5_lead === 0x00) + return finished; + + // 3. If Big5 lead is not 0x00, let lead be Big5 lead, let + // pointer be null, set Big5 lead to 0x00, and then run these + // substeps: + if (Big5_lead !== 0x00) { + var lead = Big5_lead; + var pointer = null; + Big5_lead = 0x00; + + // 1. Let offset be 0x40 if byte is less than 0x7F and 0x62 + // otherwise. + var offset = bite < 0x7F ? 0x40 : 0x62; + + // 2. If byte is in the range 0x40 to 0x7E, inclusive, or 0xA1 + // to 0xFE, inclusive, set pointer to (lead − 0x81) × 157 + + // (byte − offset). + if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0xA1, 0xFE)) + pointer = (lead - 0x81) * 157 + (bite - offset); + + // 3. If there is a row in the table below whose first column + // is pointer, return the two code points listed in its second + // column + // Pointer | Code points + // --------+-------------- + // 1133 | U+00CA U+0304 + // 1135 | U+00CA U+030C + // 1164 | U+00EA U+0304 + // 1166 | U+00EA U+030C + switch (pointer) { + case 1133: return [0x00CA, 0x0304]; + case 1135: return [0x00CA, 0x030C]; + case 1164: return [0x00EA, 0x0304]; + case 1166: return [0x00EA, 0x030C]; + } + + // 4. Let code point be null if pointer is null and the index + // code point for pointer in index Big5 otherwise. + var code_point = (pointer === null) ? null : + indexCodePointFor(pointer, index('big5')); + + // 5. If code point is null and byte is an ASCII byte, prepend + // byte to stream. + if (code_point === null && isASCIIByte(bite)) + stream.prepend(bite); + + // 6. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 7. Return a code point whose value is code point. + return code_point; + } + + // 4. If byte is an ASCII byte, return a code point whose value + // is byte. + if (isASCIIByte(bite)) + return bite; + + // 5. If byte is in the range 0x81 to 0xFE, inclusive, set Big5 + // lead to byte and return continue. + if (inRange(bite, 0x81, 0xFE)) { + Big5_lead = bite; + return null; + } + + // 6. Return error. + return decoderError(fatal); + }; + } + + // 12.1.2 Big5 encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function Big5Encoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. Let pointer be the index Big5 pointer for code point. + var pointer = indexBig5PointerFor(code_point); + + // 4. If pointer is null, return error with code point. + if (pointer === null) + return encoderError(code_point); + + // 5. Let lead be floor(pointer / 157) + 0x81. + var lead = floor(pointer / 157) + 0x81; + + // 6. If lead is less than 0xA1, return error with code point. + if (lead < 0xA1) + return encoderError(code_point); + + // 7. Let trail be pointer % 157. + var trail = pointer % 157; + + // 8. Let offset be 0x40 if trail is less than 0x3F and 0x62 + // otherwise. + var offset = trail < 0x3F ? 0x40 : 0x62; + + // Return two bytes whose values are lead and trail + offset. + return [lead, trail + offset]; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['Big5'] = function(options) { + return new Big5Encoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['Big5'] = function(options) { + return new Big5Decoder(options); + }; + + + // + // 13. Legacy multi-byte Japanese encodings + // + + // 13.1 euc-jp + + // 13.1.1 euc-jp decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function EUCJPDecoder(options) { + var fatal = options.fatal; + + // euc-jp's decoder has an associated euc-jp jis0212 flag + // (initially unset) and euc-jp lead (initially 0x00). + var /** @type {boolean} */ eucjp_jis0212_flag = false, + /** @type {number} */ eucjp_lead = 0x00; + + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and euc-jp lead is not 0x00, set + // euc-jp lead to 0x00, and return error. + if (bite === end_of_stream && eucjp_lead !== 0x00) { + eucjp_lead = 0x00; + return decoderError(fatal); + } + + // 2. If byte is end-of-stream and euc-jp lead is 0x00, return + // finished. + if (bite === end_of_stream && eucjp_lead === 0x00) + return finished; + + // 3. If euc-jp lead is 0x8E and byte is in the range 0xA1 to + // 0xDF, inclusive, set euc-jp lead to 0x00 and return a code + // point whose value is 0xFF61 − 0xA1 + byte. + if (eucjp_lead === 0x8E && inRange(bite, 0xA1, 0xDF)) { + eucjp_lead = 0x00; + return 0xFF61 - 0xA1 + bite; + } + + // 4. If euc-jp lead is 0x8F and byte is in the range 0xA1 to + // 0xFE, inclusive, set the euc-jp jis0212 flag, set euc-jp lead + // to byte, and return continue. + if (eucjp_lead === 0x8F && inRange(bite, 0xA1, 0xFE)) { + eucjp_jis0212_flag = true; + eucjp_lead = bite; + return null; + } + + // 5. If euc-jp lead is not 0x00, let lead be euc-jp lead, set + // euc-jp lead to 0x00, and run these substeps: + if (eucjp_lead !== 0x00) { + var lead = eucjp_lead; + eucjp_lead = 0x00; + + // 1. Let code point be null. + var code_point = null; + + // 2. If lead and byte are both in the range 0xA1 to 0xFE, + // inclusive, set code point to the index code point for (lead + // − 0xA1) × 94 + byte − 0xA1 in index jis0208 if the euc-jp + // jis0212 flag is unset and in index jis0212 otherwise. + if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) { + code_point = indexCodePointFor( + (lead - 0xA1) * 94 + (bite - 0xA1), + index(!eucjp_jis0212_flag ? 'jis0208' : 'jis0212')); + } + + // 3. Unset the euc-jp jis0212 flag. + eucjp_jis0212_flag = false; + + // 4. If byte is not in the range 0xA1 to 0xFE, inclusive, + // prepend byte to stream. + if (!inRange(bite, 0xA1, 0xFE)) + stream.prepend(bite); + + // 5. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 6. Return a code point whose value is code point. + return code_point; + } + + // 6. If byte is an ASCII byte, return a code point whose value + // is byte. + if (isASCIIByte(bite)) + return bite; + + // 7. If byte is 0x8E, 0x8F, or in the range 0xA1 to 0xFE, + // inclusive, set euc-jp lead to byte and return continue. + if (bite === 0x8E || bite === 0x8F || inRange(bite, 0xA1, 0xFE)) { + eucjp_lead = bite; + return null; + } + + // 8. Return error. + return decoderError(fatal); + }; + } + + // 13.1.2 euc-jp encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function EUCJPEncoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. If code point is U+00A5, return byte 0x5C. + if (code_point === 0x00A5) + return 0x5C; + + // 4. If code point is U+203E, return byte 0x7E. + if (code_point === 0x203E) + return 0x7E; + + // 5. If code point is in the range U+FF61 to U+FF9F, inclusive, + // return two bytes whose values are 0x8E and code point − + // 0xFF61 + 0xA1. + if (inRange(code_point, 0xFF61, 0xFF9F)) + return [0x8E, code_point - 0xFF61 + 0xA1]; + + // 6. If code point is U+2212, set it to U+FF0D. + if (code_point === 0x2212) + code_point = 0xFF0D; + + // 7. Let pointer be the index pointer for code point in index + // jis0208. + var pointer = indexPointerFor(code_point, index('jis0208')); + + // 8. If pointer is null, return error with code point. + if (pointer === null) + return encoderError(code_point); + + // 9. Let lead be floor(pointer / 94) + 0xA1. + var lead = floor(pointer / 94) + 0xA1; + + // 10. Let trail be pointer % 94 + 0xA1. + var trail = pointer % 94 + 0xA1; + + // 11. Return two bytes whose values are lead and trail. + return [lead, trail]; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['EUC-JP'] = function(options) { + return new EUCJPEncoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['EUC-JP'] = function(options) { + return new EUCJPDecoder(options); + }; + + // 13.2 iso-2022-jp + + // 13.2.1 iso-2022-jp decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function ISO2022JPDecoder(options) { + var fatal = options.fatal; + /** @enum */ + var states = { + ASCII: 0, + Roman: 1, + Katakana: 2, + LeadByte: 3, + TrailByte: 4, + EscapeStart: 5, + Escape: 6 + }; + // iso-2022-jp's decoder has an associated iso-2022-jp decoder + // state (initially ASCII), iso-2022-jp decoder output state + // (initially ASCII), iso-2022-jp lead (initially 0x00), and + // iso-2022-jp output flag (initially unset). + var /** @type {number} */ iso2022jp_decoder_state = states.ASCII, + /** @type {number} */ iso2022jp_decoder_output_state = states.ASCII, + /** @type {number} */ iso2022jp_lead = 0x00, + /** @type {boolean} */ iso2022jp_output_flag = false; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // switching on iso-2022-jp decoder state: + switch (iso2022jp_decoder_state) { + default: + case states.ASCII: + // ASCII + // Based on byte: + + // 0x1B + if (bite === 0x1B) { + // Set iso-2022-jp decoder state to escape start and return + // continue. + iso2022jp_decoder_state = states.EscapeStart; + return null; + } + + // 0x00 to 0x7F, excluding 0x0E, 0x0F, and 0x1B + if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E + && bite !== 0x0F && bite !== 0x1B) { + // Unset the iso-2022-jp output flag and return a code point + // whose value is byte. + iso2022jp_output_flag = false; + return bite; + } + + // end-of-stream + if (bite === end_of_stream) { + // Return finished. + return finished; + } + + // Otherwise + // Unset the iso-2022-jp output flag and return error. + iso2022jp_output_flag = false; + return decoderError(fatal); + + case states.Roman: + // Roman + // Based on byte: + + // 0x1B + if (bite === 0x1B) { + // Set iso-2022-jp decoder state to escape start and return + // continue. + iso2022jp_decoder_state = states.EscapeStart; + return null; + } + + // 0x5C + if (bite === 0x5C) { + // Unset the iso-2022-jp output flag and return code point + // U+00A5. + iso2022jp_output_flag = false; + return 0x00A5; + } + + // 0x7E + if (bite === 0x7E) { + // Unset the iso-2022-jp output flag and return code point + // U+203E. + iso2022jp_output_flag = false; + return 0x203E; + } + + // 0x00 to 0x7F, excluding 0x0E, 0x0F, 0x1B, 0x5C, and 0x7E + if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E && bite !== 0x0F + && bite !== 0x1B && bite !== 0x5C && bite !== 0x7E) { + // Unset the iso-2022-jp output flag and return a code point + // whose value is byte. + iso2022jp_output_flag = false; + return bite; + } + + // end-of-stream + if (bite === end_of_stream) { + // Return finished. + return finished; + } + + // Otherwise + // Unset the iso-2022-jp output flag and return error. + iso2022jp_output_flag = false; + return decoderError(fatal); + + case states.Katakana: + // Katakana + // Based on byte: + + // 0x1B + if (bite === 0x1B) { + // Set iso-2022-jp decoder state to escape start and return + // continue. + iso2022jp_decoder_state = states.EscapeStart; + return null; + } + + // 0x21 to 0x5F + if (inRange(bite, 0x21, 0x5F)) { + // Unset the iso-2022-jp output flag and return a code point + // whose value is 0xFF61 − 0x21 + byte. + iso2022jp_output_flag = false; + return 0xFF61 - 0x21 + bite; + } + + // end-of-stream + if (bite === end_of_stream) { + // Return finished. + return finished; + } + + // Otherwise + // Unset the iso-2022-jp output flag and return error. + iso2022jp_output_flag = false; + return decoderError(fatal); + + case states.LeadByte: + // Lead byte + // Based on byte: + + // 0x1B + if (bite === 0x1B) { + // Set iso-2022-jp decoder state to escape start and return + // continue. + iso2022jp_decoder_state = states.EscapeStart; + return null; + } + + // 0x21 to 0x7E + if (inRange(bite, 0x21, 0x7E)) { + // Unset the iso-2022-jp output flag, set iso-2022-jp lead + // to byte, iso-2022-jp decoder state to trail byte, and + // return continue. + iso2022jp_output_flag = false; + iso2022jp_lead = bite; + iso2022jp_decoder_state = states.TrailByte; + return null; + } + + // end-of-stream + if (bite === end_of_stream) { + // Return finished. + return finished; + } + + // Otherwise + // Unset the iso-2022-jp output flag and return error. + iso2022jp_output_flag = false; + return decoderError(fatal); + + case states.TrailByte: + // Trail byte + // Based on byte: + + // 0x1B + if (bite === 0x1B) { + // Set iso-2022-jp decoder state to escape start and return + // continue. + iso2022jp_decoder_state = states.EscapeStart; + return decoderError(fatal); + } + + // 0x21 to 0x7E + if (inRange(bite, 0x21, 0x7E)) { + // 1. Set the iso-2022-jp decoder state to lead byte. + iso2022jp_decoder_state = states.LeadByte; + + // 2. Let pointer be (iso-2022-jp lead − 0x21) × 94 + byte − 0x21. + var pointer = (iso2022jp_lead - 0x21) * 94 + bite - 0x21; + + // 3. Let code point be the index code point for pointer in + // index jis0208. + var code_point = indexCodePointFor(pointer, index('jis0208')); + + // 4. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 5. Return a code point whose value is code point. + return code_point; + } + + // end-of-stream + if (bite === end_of_stream) { + // Set the iso-2022-jp decoder state to lead byte, prepend + // byte to stream, and return error. + iso2022jp_decoder_state = states.LeadByte; + stream.prepend(bite); + return decoderError(fatal); + } + + // Otherwise + // Set iso-2022-jp decoder state to lead byte and return + // error. + iso2022jp_decoder_state = states.LeadByte; + return decoderError(fatal); + + case states.EscapeStart: + // Escape start + + // 1. If byte is either 0x24 or 0x28, set iso-2022-jp lead to + // byte, iso-2022-jp decoder state to escape, and return + // continue. + if (bite === 0x24 || bite === 0x28) { + iso2022jp_lead = bite; + iso2022jp_decoder_state = states.Escape; + return null; + } + + // 2. Prepend byte to stream. + stream.prepend(bite); + + // 3. Unset the iso-2022-jp output flag, set iso-2022-jp + // decoder state to iso-2022-jp decoder output state, and + // return error. + iso2022jp_output_flag = false; + iso2022jp_decoder_state = iso2022jp_decoder_output_state; + return decoderError(fatal); + + case states.Escape: + // Escape + + // 1. Let lead be iso-2022-jp lead and set iso-2022-jp lead to + // 0x00. + var lead = iso2022jp_lead; + iso2022jp_lead = 0x00; + + // 2. Let state be null. + var state = null; + + // 3. If lead is 0x28 and byte is 0x42, set state to ASCII. + if (lead === 0x28 && bite === 0x42) + state = states.ASCII; + + // 4. If lead is 0x28 and byte is 0x4A, set state to Roman. + if (lead === 0x28 && bite === 0x4A) + state = states.Roman; + + // 5. If lead is 0x28 and byte is 0x49, set state to Katakana. + if (lead === 0x28 && bite === 0x49) + state = states.Katakana; + + // 6. If lead is 0x24 and byte is either 0x40 or 0x42, set + // state to lead byte. + if (lead === 0x24 && (bite === 0x40 || bite === 0x42)) + state = states.LeadByte; + + // 7. If state is non-null, run these substeps: + if (state !== null) { + // 1. Set iso-2022-jp decoder state and iso-2022-jp decoder + // output state to states. + iso2022jp_decoder_state = iso2022jp_decoder_state = state; + + // 2. Let output flag be the iso-2022-jp output flag. + var output_flag = iso2022jp_output_flag; + + // 3. Set the iso-2022-jp output flag. + iso2022jp_output_flag = true; + + // 4. Return continue, if output flag is unset, and error + // otherwise. + return !output_flag ? null : decoderError(fatal); + } + + // 8. Prepend lead and byte to stream. + stream.prepend([lead, bite]); + + // 9. Unset the iso-2022-jp output flag, set iso-2022-jp + // decoder state to iso-2022-jp decoder output state and + // return error. + iso2022jp_output_flag = false; + iso2022jp_decoder_state = iso2022jp_decoder_output_state; + return decoderError(fatal); + } + }; + } + + // 13.2.2 iso-2022-jp encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function ISO2022JPEncoder(options) { + var fatal = options.fatal; + // iso-2022-jp's encoder has an associated iso-2022-jp encoder + // state which is one of ASCII, Roman, and jis0208 (initially + // ASCII). + /** @enum */ + var states = { + ASCII: 0, + Roman: 1, + jis0208: 2 + }; + var /** @type {number} */ iso2022jp_state = states.ASCII; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream and iso-2022-jp encoder + // state is not ASCII, prepend code point to stream, set + // iso-2022-jp encoder state to ASCII, and return three bytes + // 0x1B 0x28 0x42. + if (code_point === end_of_stream && + iso2022jp_state !== states.ASCII) { + stream.prepend(code_point); + iso2022jp_state = states.ASCII; + return [0x1B, 0x28, 0x42]; + } + + // 2. If code point is end-of-stream and iso-2022-jp encoder + // state is ASCII, return finished. + if (code_point === end_of_stream && iso2022jp_state === states.ASCII) + return finished; + + // 3. If ISO-2022-JP encoder state is ASCII or Roman, and code + // point is U+000E, U+000F, or U+001B, return error with U+FFFD. + if ((iso2022jp_state === states.ASCII || + iso2022jp_state === states.Roman) && + (code_point === 0x000E || code_point === 0x000F || + code_point === 0x001B)) { + return encoderError(0xFFFD); + } + + // 4. If iso-2022-jp encoder state is ASCII and code point is an + // ASCII code point, return a byte whose value is code point. + if (iso2022jp_state === states.ASCII && + isASCIICodePoint(code_point)) + return code_point; + + // 5. If iso-2022-jp encoder state is Roman and code point is an + // ASCII code point, excluding U+005C and U+007E, or is U+00A5 + // or U+203E, run these substeps: + if (iso2022jp_state === states.Roman && + ((isASCIICodePoint(code_point) && + code_point !== 0x005C && code_point !== 0x007E) || + (code_point == 0x00A5 || code_point == 0x203E))) { + + // 1. If code point is an ASCII code point, return a byte + // whose value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 2. If code point is U+00A5, return byte 0x5C. + if (code_point === 0x00A5) + return 0x5C; + + // 3. If code point is U+203E, return byte 0x7E. + if (code_point === 0x203E) + return 0x7E; + } + + // 6. If code point is an ASCII code point, and iso-2022-jp + // encoder state is not ASCII, prepend code point to stream, set + // iso-2022-jp encoder state to ASCII, and return three bytes + // 0x1B 0x28 0x42. + if (isASCIICodePoint(code_point) && + iso2022jp_state !== states.ASCII) { + stream.prepend(code_point); + iso2022jp_state = states.ASCII; + return [0x1B, 0x28, 0x42]; + } + + // 7. If code point is either U+00A5 or U+203E, and iso-2022-jp + // encoder state is not Roman, prepend code point to stream, set + // iso-2022-jp encoder state to Roman, and return three bytes + // 0x1B 0x28 0x4A. + if ((code_point === 0x00A5 || code_point === 0x203E) && + iso2022jp_state !== states.Roman) { + stream.prepend(code_point); + iso2022jp_state = states.Roman; + return [0x1B, 0x28, 0x4A]; + } + + // 8. If code point is U+2212, set it to U+FF0D. + if (code_point === 0x2212) + code_point = 0xFF0D; + + // 9. Let pointer be the index pointer for code point in index + // jis0208. + var pointer = indexPointerFor(code_point, index('jis0208')); + + // 10. If pointer is null, return error with code point. + if (pointer === null) + return encoderError(code_point); + + // 11. If iso-2022-jp encoder state is not jis0208, prepend code + // point to stream, set iso-2022-jp encoder state to jis0208, + // and return three bytes 0x1B 0x24 0x42. + if (iso2022jp_state !== states.jis0208) { + stream.prepend(code_point); + iso2022jp_state = states.jis0208; + return [0x1B, 0x24, 0x42]; + } + + // 12. Let lead be floor(pointer / 94) + 0x21. + var lead = floor(pointer / 94) + 0x21; + + // 13. Let trail be pointer % 94 + 0x21. + var trail = pointer % 94 + 0x21; + + // 14. Return two bytes whose values are lead and trail. + return [lead, trail]; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['ISO-2022-JP'] = function(options) { + return new ISO2022JPEncoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['ISO-2022-JP'] = function(options) { + return new ISO2022JPDecoder(options); + }; + + // 13.3 Shift_JIS + + // 13.3.1 Shift_JIS decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function ShiftJISDecoder(options) { + var fatal = options.fatal; + // Shift_JIS's decoder has an associated Shift_JIS lead (initially + // 0x00). + var /** @type {number} */ Shift_JIS_lead = 0x00; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and Shift_JIS lead is not 0x00, + // set Shift_JIS lead to 0x00 and return error. + if (bite === end_of_stream && Shift_JIS_lead !== 0x00) { + Shift_JIS_lead = 0x00; + return decoderError(fatal); + } + + // 2. If byte is end-of-stream and Shift_JIS lead is 0x00, + // return finished. + if (bite === end_of_stream && Shift_JIS_lead === 0x00) + return finished; + + // 3. If Shift_JIS lead is not 0x00, let lead be Shift_JIS lead, + // let pointer be null, set Shift_JIS lead to 0x00, and then run + // these substeps: + if (Shift_JIS_lead !== 0x00) { + var lead = Shift_JIS_lead; + var pointer = null; + Shift_JIS_lead = 0x00; + + // 1. Let offset be 0x40, if byte is less than 0x7F, and 0x41 + // otherwise. + var offset = (bite < 0x7F) ? 0x40 : 0x41; + + // 2. Let lead offset be 0x81, if lead is less than 0xA0, and + // 0xC1 otherwise. + var lead_offset = (lead < 0xA0) ? 0x81 : 0xC1; + + // 3. If byte is in the range 0x40 to 0x7E, inclusive, or 0x80 + // to 0xFC, inclusive, set pointer to (lead − lead offset) × + // 188 + byte − offset. + if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFC)) + pointer = (lead - lead_offset) * 188 + bite - offset; + + // 4. If pointer is in the range 8836 to 10715, inclusive, + // return a code point whose value is 0xE000 − 8836 + pointer. + if (inRange(pointer, 8836, 10715)) + return 0xE000 - 8836 + pointer; + + // 5. Let code point be null, if pointer is null, and the + // index code point for pointer in index jis0208 otherwise. + var code_point = (pointer === null) ? null : + indexCodePointFor(pointer, index('jis0208')); + + // 6. If code point is null and byte is an ASCII byte, prepend + // byte to stream. + if (code_point === null && isASCIIByte(bite)) + stream.prepend(bite); + + // 7. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 8. Return a code point whose value is code point. + return code_point; + } + + // 4. If byte is an ASCII byte or 0x80, return a code point + // whose value is byte. + if (isASCIIByte(bite) || bite === 0x80) + return bite; + + // 5. If byte is in the range 0xA1 to 0xDF, inclusive, return a + // code point whose value is 0xFF61 − 0xA1 + byte. + if (inRange(bite, 0xA1, 0xDF)) + return 0xFF61 - 0xA1 + bite; + + // 6. If byte is in the range 0x81 to 0x9F, inclusive, or 0xE0 + // to 0xFC, inclusive, set Shift_JIS lead to byte and return + // continue. + if (inRange(bite, 0x81, 0x9F) || inRange(bite, 0xE0, 0xFC)) { + Shift_JIS_lead = bite; + return null; + } + + // 7. Return error. + return decoderError(fatal); + }; + } + + // 13.3.2 Shift_JIS encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function ShiftJISEncoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point or U+0080, return a + // byte whose value is code point. + if (isASCIICodePoint(code_point) || code_point === 0x0080) + return code_point; + + // 3. If code point is U+00A5, return byte 0x5C. + if (code_point === 0x00A5) + return 0x5C; + + // 4. If code point is U+203E, return byte 0x7E. + if (code_point === 0x203E) + return 0x7E; + + // 5. If code point is in the range U+FF61 to U+FF9F, inclusive, + // return a byte whose value is code point − 0xFF61 + 0xA1. + if (inRange(code_point, 0xFF61, 0xFF9F)) + return code_point - 0xFF61 + 0xA1; + + // 6. If code point is U+2212, set it to U+FF0D. + if (code_point === 0x2212) + code_point = 0xFF0D; + + // 7. Let pointer be the index Shift_JIS pointer for code point. + var pointer = indexShiftJISPointerFor(code_point); + + // 8. If pointer is null, return error with code point. + if (pointer === null) + return encoderError(code_point); + + // 9. Let lead be floor(pointer / 188). + var lead = floor(pointer / 188); + + // 10. Let lead offset be 0x81, if lead is less than 0x1F, and + // 0xC1 otherwise. + var lead_offset = (lead < 0x1F) ? 0x81 : 0xC1; + + // 11. Let trail be pointer % 188. + var trail = pointer % 188; + + // 12. Let offset be 0x40, if trail is less than 0x3F, and 0x41 + // otherwise. + var offset = (trail < 0x3F) ? 0x40 : 0x41; + + // 13. Return two bytes whose values are lead + lead offset and + // trail + offset. + return [lead + lead_offset, trail + offset]; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['Shift_JIS'] = function(options) { + return new ShiftJISEncoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['Shift_JIS'] = function(options) { + return new ShiftJISDecoder(options); + }; + + // + // 14. Legacy multi-byte Korean encodings + // + + // 14.1 euc-kr + + // 14.1.1 euc-kr decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function EUCKRDecoder(options) { + var fatal = options.fatal; + + // euc-kr's decoder has an associated euc-kr lead (initially 0x00). + var /** @type {number} */ euckr_lead = 0x00; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and euc-kr lead is not 0x00, set + // euc-kr lead to 0x00 and return error. + if (bite === end_of_stream && euckr_lead !== 0) { + euckr_lead = 0x00; + return decoderError(fatal); + } + + // 2. If byte is end-of-stream and euc-kr lead is 0x00, return + // finished. + if (bite === end_of_stream && euckr_lead === 0) + return finished; + + // 3. If euc-kr lead is not 0x00, let lead be euc-kr lead, let + // pointer be null, set euc-kr lead to 0x00, and then run these + // substeps: + if (euckr_lead !== 0x00) { + var lead = euckr_lead; + var pointer = null; + euckr_lead = 0x00; + + // 1. If byte is in the range 0x41 to 0xFE, inclusive, set + // pointer to (lead − 0x81) × 190 + (byte − 0x41). + if (inRange(bite, 0x41, 0xFE)) + pointer = (lead - 0x81) * 190 + (bite - 0x41); + + // 2. Let code point be null, if pointer is null, and the + // index code point for pointer in index euc-kr otherwise. + var code_point = (pointer === null) + ? null : indexCodePointFor(pointer, index('euc-kr')); + + // 3. If code point is null and byte is an ASCII byte, prepend + // byte to stream. + if (pointer === null && isASCIIByte(bite)) + stream.prepend(bite); + + // 4. If code point is null, return error. + if (code_point === null) + return decoderError(fatal); + + // 5. Return a code point whose value is code point. + return code_point; + } + + // 4. If byte is an ASCII byte, return a code point whose value + // is byte. + if (isASCIIByte(bite)) + return bite; + + // 5. If byte is in the range 0x81 to 0xFE, inclusive, set + // euc-kr lead to byte and return continue. + if (inRange(bite, 0x81, 0xFE)) { + euckr_lead = bite; + return null; + } + + // 6. Return error. + return decoderError(fatal); + }; + } + + // 14.1.2 euc-kr encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function EUCKREncoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. Let pointer be the index pointer for code point in index + // euc-kr. + var pointer = indexPointerFor(code_point, index('euc-kr')); + + // 4. If pointer is null, return error with code point. + if (pointer === null) + return encoderError(code_point); + + // 5. Let lead be floor(pointer / 190) + 0x81. + var lead = floor(pointer / 190) + 0x81; + + // 6. Let trail be pointer % 190 + 0x41. + var trail = (pointer % 190) + 0x41; + + // 7. Return two bytes whose values are lead and trail. + return [lead, trail]; + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['EUC-KR'] = function(options) { + return new EUCKREncoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['EUC-KR'] = function(options) { + return new EUCKRDecoder(options); + }; + + + // + // 15. Legacy miscellaneous encodings + // + + // 15.1 replacement + + // Not needed - API throws RangeError + + // 15.2 Common infrastructure for utf-16be and utf-16le + + /** + * @param {number} code_unit + * @param {boolean} utf16be + * @return {!Array.<number>} bytes + */ + function convertCodeUnitToBytes(code_unit, utf16be) { + // 1. Let byte1 be code unit >> 8. + var byte1 = code_unit >> 8; + + // 2. Let byte2 be code unit & 0x00FF. + var byte2 = code_unit & 0x00FF; + + // 3. Then return the bytes in order: + // utf-16be flag is set: byte1, then byte2. + if (utf16be) + return [byte1, byte2]; + // utf-16be flag is unset: byte2, then byte1. + return [byte2, byte1]; + } + + // 15.2.1 shared utf-16 decoder + /** + * @constructor + * @implements {Decoder} + * @param {boolean} utf16_be True if big-endian, false if little-endian. + * @param {{fatal: boolean}} options + */ + function UTF16Decoder(utf16_be, options) { + var fatal = options.fatal; + var /** @type {?number} */ utf16_lead_byte = null, + /** @type {?number} */ utf16_lead_surrogate = null; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream and either utf-16 lead byte or + // utf-16 lead surrogate is not null, set utf-16 lead byte and + // utf-16 lead surrogate to null, and return error. + if (bite === end_of_stream && (utf16_lead_byte !== null || + utf16_lead_surrogate !== null)) { + return decoderError(fatal); + } + + // 2. If byte is end-of-stream and utf-16 lead byte and utf-16 + // lead surrogate are null, return finished. + if (bite === end_of_stream && utf16_lead_byte === null && + utf16_lead_surrogate === null) { + return finished; + } + + // 3. If utf-16 lead byte is null, set utf-16 lead byte to byte + // and return continue. + if (utf16_lead_byte === null) { + utf16_lead_byte = bite; + return null; + } + + // 4. Let code unit be the result of: + var code_unit; + if (utf16_be) { + // utf-16be decoder flag is set + // (utf-16 lead byte << 8) + byte. + code_unit = (utf16_lead_byte << 8) + bite; + } else { + // utf-16be decoder flag is unset + // (byte << 8) + utf-16 lead byte. + code_unit = (bite << 8) + utf16_lead_byte; + } + // Then set utf-16 lead byte to null. + utf16_lead_byte = null; + + // 5. If utf-16 lead surrogate is not null, let lead surrogate + // be utf-16 lead surrogate, set utf-16 lead surrogate to null, + // and then run these substeps: + if (utf16_lead_surrogate !== null) { + var lead_surrogate = utf16_lead_surrogate; + utf16_lead_surrogate = null; + + // 1. If code unit is in the range U+DC00 to U+DFFF, + // inclusive, return a code point whose value is 0x10000 + + // ((lead surrogate − 0xD800) << 10) + (code unit − 0xDC00). + if (inRange(code_unit, 0xDC00, 0xDFFF)) { + return 0x10000 + (lead_surrogate - 0xD800) * 0x400 + + (code_unit - 0xDC00); + } + + // 2. Prepend the sequence resulting of converting code unit + // to bytes using utf-16be decoder flag to stream and return + // error. + stream.prepend(convertCodeUnitToBytes(code_unit, utf16_be)); + return decoderError(fatal); + } + + // 6. If code unit is in the range U+D800 to U+DBFF, inclusive, + // set utf-16 lead surrogate to code unit and return continue. + if (inRange(code_unit, 0xD800, 0xDBFF)) { + utf16_lead_surrogate = code_unit; + return null; + } + + // 7. If code unit is in the range U+DC00 to U+DFFF, inclusive, + // return error. + if (inRange(code_unit, 0xDC00, 0xDFFF)) + return decoderError(fatal); + + // 8. Return code point code unit. + return code_unit; + }; + } + + // 15.2.2 shared utf-16 encoder + /** + * @constructor + * @implements {Encoder} + * @param {boolean} utf16_be True if big-endian, false if little-endian. + * @param {{fatal: boolean}} options + */ + function UTF16Encoder(utf16_be, options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1. If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is in the range U+0000 to U+FFFF, inclusive, + // return the sequence resulting of converting code point to + // bytes using utf-16be encoder flag. + if (inRange(code_point, 0x0000, 0xFFFF)) + return convertCodeUnitToBytes(code_point, utf16_be); + + // 3. Let lead be ((code point − 0x10000) >> 10) + 0xD800, + // converted to bytes using utf-16be encoder flag. + var lead = convertCodeUnitToBytes( + ((code_point - 0x10000) >> 10) + 0xD800, utf16_be); + + // 4. Let trail be ((code point − 0x10000) & 0x3FF) + 0xDC00, + // converted to bytes using utf-16be encoder flag. + var trail = convertCodeUnitToBytes( + ((code_point - 0x10000) & 0x3FF) + 0xDC00, utf16_be); + + // 5. Return a byte sequence of lead followed by trail. + return lead.concat(trail); + }; + } + + // 15.3 utf-16be + // 15.3.1 utf-16be decoder + /** @param {{fatal: boolean}} options */ + encoders['UTF-16BE'] = function(options) { + return new UTF16Encoder(true, options); + }; + // 15.3.2 utf-16be encoder + /** @param {{fatal: boolean}} options */ + decoders['UTF-16BE'] = function(options) { + return new UTF16Decoder(true, options); + }; + + // 15.4 utf-16le + // 15.4.1 utf-16le decoder + /** @param {{fatal: boolean}} options */ + encoders['UTF-16LE'] = function(options) { + return new UTF16Encoder(false, options); + }; + // 15.4.2 utf-16le encoder + /** @param {{fatal: boolean}} options */ + decoders['UTF-16LE'] = function(options) { + return new UTF16Decoder(false, options); + }; + + // 15.5 x-user-defined + + // 15.5.1 x-user-defined decoder + /** + * @constructor + * @implements {Decoder} + * @param {{fatal: boolean}} options + */ + function XUserDefinedDecoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream The stream of bytes being decoded. + * @param {number} bite The next byte read from the stream. + * @return {?(number|!Array.<number>)} The next code point(s) + * decoded, or null if not enough data exists in the input + * stream to decode a complete code point. + */ + this.handler = function(stream, bite) { + // 1. If byte is end-of-stream, return finished. + if (bite === end_of_stream) + return finished; + + // 2. If byte is an ASCII byte, return a code point whose value + // is byte. + if (isASCIIByte(bite)) + return bite; + + // 3. Return a code point whose value is 0xF780 + byte − 0x80. + return 0xF780 + bite - 0x80; + }; + } + + // 15.5.2 x-user-defined encoder + /** + * @constructor + * @implements {Encoder} + * @param {{fatal: boolean}} options + */ + function XUserDefinedEncoder(options) { + var fatal = options.fatal; + /** + * @param {Stream} stream Input stream. + * @param {number} code_point Next code point read from the stream. + * @return {(number|!Array.<number>)} Byte(s) to emit. + */ + this.handler = function(stream, code_point) { + // 1.If code point is end-of-stream, return finished. + if (code_point === end_of_stream) + return finished; + + // 2. If code point is an ASCII code point, return a byte whose + // value is code point. + if (isASCIICodePoint(code_point)) + return code_point; + + // 3. If code point is in the range U+F780 to U+F7FF, inclusive, + // return a byte whose value is code point − 0xF780 + 0x80. + if (inRange(code_point, 0xF780, 0xF7FF)) + return code_point - 0xF780 + 0x80; + + // 4. Return error with code point. + return encoderError(code_point); + }; + } + + /** @param {{fatal: boolean}} options */ + encoders['x-user-defined'] = function(options) { + return new XUserDefinedEncoder(options); + }; + /** @param {{fatal: boolean}} options */ + decoders['x-user-defined'] = function(options) { + return new XUserDefinedDecoder(options); + }; + + if (!global['TextEncoder']) + global['TextEncoder'] = TextEncoder; + if (!global['TextDecoder']) + global['TextDecoder'] = TextDecoder; + + if (typeof module !== "undefined" && module.exports) { + module.exports = { + TextEncoder: global['TextEncoder'], + TextDecoder: global['TextDecoder'], + EncodingIndexes: global["encoding-indexes"] + }; + } +}(this)); diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js new file mode 100644 index 0000000000000000000000000000000000000000..7e24fd9b36e18c57ca499480d867732044a0ccf4 --- /dev/null +++ b/vendor/assets/javascripts/xterm/fit.js @@ -0,0 +1,86 @@ +/* + * Fit terminal columns and rows to the dimensions of its + * DOM element. + * + * Approach: + * - Rows: Truncate the division of the terminal parent element height + * by the terminal row height + * + * - Columns: Truncate the division of the terminal parent element width by + * the terminal character width (apply display: inline at the + * terminal row and truncate its width with the current number + * of columns) + */ +(function (fit) { + if (typeof exports === 'object' && typeof module === 'object') { + /* + * CommonJS environment + */ + module.exports = fit(require('../../xterm')); + } else if (typeof define == 'function') { + /* + * Require.js is available + */ + define(['../../xterm'], fit); + } else { + /* + * Plain browser environment + */ + fit(window.Terminal); + } +})(function (Xterm) { + /** + * This module provides methods for fitting a terminal's size to a parent container. + * + * @module xterm/addons/fit/fit + */ + var exports = {}; + + exports.proposeGeometry = function (term) { + var parentElementStyle = window.getComputedStyle(term.element.parentElement), + parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')), + parentElementWidth = parseInt(parentElementStyle.getPropertyValue('width')), + elementStyle = window.getComputedStyle(term.element), + elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')), + elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')), + availableHeight = parentElementHeight - elementPaddingVer, + availableWidth = parentElementWidth - elementPaddingHor, + container = term.rowContainer, + subjectRow = term.rowContainer.firstElementChild, + contentBuffer = subjectRow.innerHTML, + characterHeight, + rows, + characterWidth, + cols, + geometry; + + subjectRow.style.display = 'inline'; + subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace + characterWidth = subjectRow.getBoundingClientRect().width; + subjectRow.style.display = ''; // Revert style before calculating height, since they differ. + characterHeight = parseInt(subjectRow.offsetHeight); + subjectRow.innerHTML = contentBuffer; + + rows = parseInt(availableHeight / characterHeight); + cols = parseInt(availableWidth / characterWidth) - 1; + + geometry = {cols: cols, rows: rows}; + return geometry; + }; + + exports.fit = function (term) { + var geometry = exports.proposeGeometry(term); + + term.resize(geometry.cols, geometry.rows); + }; + + Xterm.prototype.proposeGeometry = function () { + return exports.proposeGeometry(this); + }; + + Xterm.prototype.fit = function () { + return exports.fit(this); + }; + + return exports; +}); diff --git a/vendor/assets/javascripts/xterm/xterm.js b/vendor/assets/javascripts/xterm/xterm.js new file mode 100644 index 0000000000000000000000000000000000000000..11ce3c73db95196fce6a97105771f0a6997aed0b --- /dev/null +++ b/vendor/assets/javascripts/xterm/xterm.js @@ -0,0 +1,2235 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Terminal = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +/** + * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend + * events, displaying the in-progress composition to the UI and forwarding the final composition + * to the handler. + * @param {HTMLTextAreaElement} textarea The textarea that xterm uses for input. + * @param {HTMLElement} compositionView The element to display the in-progress composition in. + * @param {Terminal} terminal The Terminal to forward the finished composition to. + */ +function CompositionHelper(textarea, compositionView, terminal) { + this.textarea = textarea; + this.compositionView = compositionView; + this.terminal = terminal; + + // Whether input composition is currently happening, eg. via a mobile keyboard, speech input + // or IME. This variable determines whether the compositionText should be displayed on the UI. + this.isComposing = false; + + // The input currently being composed, eg. via a mobile keyboard, speech input or IME. + this.compositionText = null; + + // The position within the input textarea's value of the current composition. + this.compositionPosition = { start: null, end: null }; + + // Whether a composition is in the process of being sent, setting this to false will cancel + // any in-progress composition. + this.isSendingComposition = false; +} + +/** + * Handles the compositionstart event, activating the composition view. + */ +CompositionHelper.prototype.compositionstart = function () { + this.isComposing = true; + this.compositionPosition.start = this.textarea.value.length; + this.compositionView.textContent = ''; + this.compositionView.classList.add('active'); +}; + +/** + * Handles the compositionupdate event, updating the composition view. + * @param {CompositionEvent} ev The event. + */ +CompositionHelper.prototype.compositionupdate = function (ev) { + this.compositionView.textContent = ev.data; + this.updateCompositionElements(); + var self = this; + setTimeout(function () { + self.compositionPosition.end = self.textarea.value.length; + }, 0); +}; + +/** + * Handles the compositionend event, hiding the composition view and sending the composition to + * the handler. + */ +CompositionHelper.prototype.compositionend = function () { + this.finalizeComposition(true); +}; + +/** + * Handles the keydown event, routing any necessary events to the CompositionHelper functions. + * @return Whether the Terminal should continue processing the keydown event. + */ +CompositionHelper.prototype.keydown = function (ev) { + if (this.isComposing || this.isSendingComposition) { + if (ev.keyCode === 229) { + // Continue composing if the keyCode is the "composition character" + return false; + } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { + // Continue composing if the keyCode is a modifier key + return false; + } else { + // Finish composition immediately. This is mainly here for the case where enter is + // pressed and the handler needs to be triggered before the command is executed. + this.finalizeComposition(false); + } + } + + if (ev.keyCode === 229) { + // If the "composition character" is used but gets to this point it means a non-composition + // character (eg. numbers and punctuation) was pressed when the IME was active. + this.handleAnyTextareaChanges(); + return false; + } + + return true; +}; + +/** + * Finalizes the composition, resuming regular input actions. This is called when a composition + * is ending. + * @param {boolean} waitForPropogation Whether to wait for events to propogate before sending + * the input. This should be false if a non-composition keystroke is entered before the + * compositionend event is triggered, such as enter, so that the composition is send before + * the command is executed. + */ +CompositionHelper.prototype.finalizeComposition = function (waitForPropogation) { + this.compositionView.classList.remove('active'); + this.isComposing = false; + this.clearTextareaPosition(); + + if (!waitForPropogation) { + // Cancel any delayed composition send requests and send the input immediately. + this.isSendingComposition = false; + var input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end); + this.terminal.handler(input); + } else { + // Make a deep copy of the composition position here as a new compositionstart event may + // fire before the setTimeout executes. + var currentCompositionPosition = { + start: this.compositionPosition.start, + end: this.compositionPosition.end + }; + + // Since composition* events happen before the changes take place in the textarea on most + // browsers, use a setTimeout with 0ms time to allow the native compositionend event to + // complete. This ensures the correct character is retrieved, this solution was used + // because: + // - The compositionend event's data property is unreliable, at least on Chromium + // - The last compositionupdate event's data property does not always accurately describe + // the character, a counter example being Korean where an ending consonsant can move to + // the following character if the following input is a vowel. + var self = this; + this.isSendingComposition = true; + setTimeout(function () { + // Ensure that the input has not already been sent + if (self.isSendingComposition) { + self.isSendingComposition = false; + var input; + if (self.isComposing) { + // Use the end position to get the string if a new composition has started. + input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + } else { + // Don't use the end position here in order to pick up any characters after the + // composition has finished, for example when typing a non-composition character + // (eg. 2) after a composition character. + input = self.textarea.value.substring(currentCompositionPosition.start); + } + self.terminal.handler(input); + } + }, 0); + } +}; + +/** + * Apply any changes made to the textarea after the current event chain is allowed to complete. + * This should be called when not currently composing but a keydown event with the "composition + * character" (229) is triggered, in order to allow non-composition text to be entered when an + * IME is active. + */ +CompositionHelper.prototype.handleAnyTextareaChanges = function () { + var oldValue = this.textarea.value; + var self = this; + setTimeout(function () { + // Ignore if a composition has started since the timeout + if (!self.isComposing) { + var newValue = self.textarea.value; + var diff = newValue.replace(oldValue, ''); + if (diff.length > 0) { + self.terminal.handler(diff); + } + } + }, 0); +}; + +/** + * Positions the composition view on top of the cursor and the textarea just below it (so the + * IME helper dialog is positioned correctly). + */ +CompositionHelper.prototype.updateCompositionElements = function (dontRecurse) { + if (!this.isComposing) { + return; + } + var cursor = this.terminal.element.querySelector('.terminal-cursor'); + if (cursor) { + // Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within + // the .xterm element. + var xtermRows = this.terminal.element.querySelector('.xterm-rows'); + var cursorTop = xtermRows.offsetTop + cursor.offsetTop; + + this.compositionView.style.left = cursor.offsetLeft + 'px'; + this.compositionView.style.top = cursorTop + 'px'; + this.compositionView.style.height = cursor.offsetHeight + 'px'; + this.compositionView.style.lineHeight = cursor.offsetHeight + 'px'; + // Sync the textarea to the exact position of the composition view so the IME knows where the + // text is. + var compositionViewBounds = this.compositionView.getBoundingClientRect(); + this.textarea.style.left = cursor.offsetLeft + 'px'; + this.textarea.style.top = cursorTop + 'px'; + this.textarea.style.width = compositionViewBounds.width + 'px'; + this.textarea.style.height = compositionViewBounds.height + 'px'; + this.textarea.style.lineHeight = compositionViewBounds.height + 'px'; + } + if (!dontRecurse) { + setTimeout(this.updateCompositionElements.bind(this, true), 0); + } +}; + +/** + * Clears the textarea's position so that the cursor does not blink on IE. + * @private + */ +CompositionHelper.prototype.clearTextareaPosition = function () { + this.textarea.style.left = ''; + this.textarea.style.top = ''; +}; + +exports.CompositionHelper = CompositionHelper; + +},{}],2:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +function EventEmitter() { + this._events = this._events || {}; +} + +EventEmitter.prototype.addListener = function (type, listener) { + this._events[type] = this._events[type] || []; + this._events[type].push(listener); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.removeListener = function (type, listener) { + if (!this._events[type]) return; + + var obj = this._events[type], + i = obj.length; + + while (i--) { + if (obj[i] === listener || obj[i].listener === listener) { + obj.splice(i, 1); + return; + } + } +}; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +EventEmitter.prototype.removeAllListeners = function (type) { + if (this._events[type]) delete this._events[type]; +}; + +EventEmitter.prototype.once = function (type, listener) { + var self = this; + function on() { + var args = Array.prototype.slice.call(arguments); + this.removeListener(type, on); + return listener.apply(this, args); + } + on.listener = listener; + return this.on(type, on); +}; + +EventEmitter.prototype.emit = function (type) { + if (!this._events[type]) return; + + var args = Array.prototype.slice.call(arguments, 1), + obj = this._events[type], + l = obj.length, + i = 0; + + for (; i < l; i++) { + obj[i].apply(this, args); + } +}; + +EventEmitter.prototype.listeners = function (type) { + return this._events[type] = this._events[type] || []; +}; + +exports.EventEmitter = EventEmitter; + +},{}],3:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +/** + * Represents the viewport of a terminal, the visible area within the larger buffer of output. + * Logic for the virtual scroll bar is included in this object. + * @param {Terminal} terminal The Terminal object. + * @param {HTMLElement} viewportElement The DOM element acting as the viewport + * @param {HTMLElement} charMeasureElement A DOM element used to measure the character size of + * the terminal. + */ +function Viewport(terminal, viewportElement, scrollArea, charMeasureElement) { + this.terminal = terminal; + this.viewportElement = viewportElement; + this.scrollArea = scrollArea; + this.charMeasureElement = charMeasureElement; + this.currentRowHeight = 0; + this.lastRecordedBufferLength = 0; + this.lastRecordedViewportHeight = 0; + + this.terminal.on('scroll', this.syncScrollArea.bind(this)); + this.terminal.on('resize', this.syncScrollArea.bind(this)); + this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); + + this.syncScrollArea(); +} + +/** + * Refreshes row height, setting line-height, viewport height and scroll area height if + * necessary. + * @param {number|undefined} charSize A character size measurement bounding rect object, if it + * doesn't exist it will be created. + */ +Viewport.prototype.refresh = function (charSize) { + var size = charSize || this.charMeasureElement.getBoundingClientRect(); + if (size.height > 0) { + var rowHeightChanged = size.height !== this.currentRowHeight; + if (rowHeightChanged) { + this.currentRowHeight = size.height; + this.viewportElement.style.lineHeight = size.height + 'px'; + this.terminal.rowContainer.style.lineHeight = size.height + 'px'; + } + var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; + if (rowHeightChanged || viewportHeightChanged) { + this.lastRecordedViewportHeight = this.terminal.rows; + this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; + } + this.scrollArea.style.height = size.height * this.lastRecordedBufferLength + 'px'; + } +}; + +/** + * Updates dimensions and synchronizes the scroll area if necessary. + */ +Viewport.prototype.syncScrollArea = function () { + if (this.lastRecordedBufferLength !== this.terminal.lines.length) { + // If buffer height changed + this.lastRecordedBufferLength = this.terminal.lines.length; + this.refresh(); + } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { + // If viewport height changed + this.refresh(); + } else { + // If size has changed, refresh viewport + var size = this.charMeasureElement.getBoundingClientRect(); + if (size.height !== this.currentRowHeight) { + this.refresh(size); + } + } + + // Sync scrollTop + var scrollTop = this.terminal.ydisp * this.currentRowHeight; + if (this.viewportElement.scrollTop !== scrollTop) { + this.viewportElement.scrollTop = scrollTop; + } +}; + +/** + * Handles scroll events on the viewport, calculating the new viewport and requesting the + * terminal to scroll to it. + * @param {Event} ev The scroll event. + */ +Viewport.prototype.onScroll = function (ev) { + var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); + var diff = newRow - this.terminal.ydisp; + this.terminal.scrollDisp(diff, true); +}; + +/** + * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual + * scrolling to `onScroll`, this event needs to be attached manually by the consumer of + * `Viewport`. + * @param {WheelEvent} ev The mouse wheel event. + */ +Viewport.prototype.onWheel = function (ev) { + if (ev.deltaY === 0) { + // Do nothing if it's not a vertical scroll event + return; + } + // Fallback to WheelEvent.DOM_DELTA_PIXEL + var multiplier = 1; + if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { + multiplier = this.currentRowHeight; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + multiplier = this.currentRowHeight * this.terminal.rows; + } + this.viewportElement.scrollTop += ev.deltaY * multiplier; + // Prevent the page from scrolling when the terminal scrolls + ev.preventDefault(); +}; + +exports.Viewport = Viewport; + +},{}],4:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License) + */ + +/** + * Clipboard handler module. This module contains methods for handling all + * clipboard-related events appropriately in the terminal. + * @module xterm/handlers/Clipboard + */ + +/** + * Prepares text copied from terminal selection, to be saved in the clipboard by: + * 1. stripping all trailing white spaces + * 2. converting all non-breaking spaces to regular spaces + * @param {string} text The copied text that needs processing for storing in clipboard + * @returns {string} + */ +function prepareTextForClipboard(text) { + var space = String.fromCharCode(32), + nonBreakingSpace = String.fromCharCode(160), + allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), + processedText = text.split('\n').map(function (line) { + // Strip all trailing white spaces and convert all non-breaking spaces + // to regular spaces. + var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space); + + return processedLine; + }).join('\n'); + + return processedText; +} + +/** + * Binds copy functionality to the given terminal. + * @param {ClipboardEvent} ev The original copy event to be handled + */ +function copyHandler(ev, term) { + var copiedText = window.getSelection().toString(), + text = prepareTextForClipboard(copiedText); + + if (term.browser.isMSIE) { + window.clipboardData.setData('Text', text); + } else { + ev.clipboardData.setData('text/plain', text); + } + + ev.preventDefault(); // Prevent or the original text will be copied. +} + +/** + * Redirect the clipboard's data to the terminal's input handler. + * @param {ClipboardEvent} ev The original paste event to be handled + * @param {Terminal} term The terminal on which to apply the handled paste event + */ +function pasteHandler(ev, term) { + ev.stopPropagation(); + + var dispatchPaste = function dispatchPaste(text) { + term.handler(text); + term.textarea.value = ''; + return term.cancel(ev); + }; + + if (term.browser.isMSIE) { + if (window.clipboardData) { + var text = window.clipboardData.getData('Text'); + dispatchPaste(text); + } + } else { + if (ev.clipboardData) { + var text = ev.clipboardData.getData('text/plain'); + dispatchPaste(text); + } + } +} + +/** + * Bind to right-click event and allow right-click copy and paste. + * + * **Logic** + * If text is selected and right-click happens on selected text, then + * do nothing to allow seamless copying. + * If no text is selected or right-click is outside of the selection + * area, then bring the terminal's input below the cursor, in order to + * trigger the event on the textarea and allow-right click paste, without + * caring about disappearing selection. + * @param {ClipboardEvent} ev The original paste event to be handled + * @param {Terminal} term The terminal on which to apply the handled paste event + */ +function rightClickHandler(ev, term) { + var s = document.getSelection(), + selectedText = prepareTextForClipboard(s.toString()), + clickIsOnSelection = false; + + if (s.rangeCount) { + var r = s.getRangeAt(0), + cr = r.getClientRects(), + x = ev.clientX, + y = ev.clientY, + i, + rect; + + for (i = 0; i < cr.length; i++) { + rect = cr[i]; + clickIsOnSelection = x > rect.left && x < rect.right && y > rect.top && y < rect.bottom; + + if (clickIsOnSelection) { + break; + } + } + // If we clicked on selection and selection is not a single space, + // then mark the right click as copy-only. We check for the single + // space selection, as this can happen when clicking on an + // and there is not much pointing in copying a single space. + if (selectedText.match(/^\s$/) || !selectedText.length) { + clickIsOnSelection = false; + } + } + + // Bring textarea at the cursor position + if (!clickIsOnSelection) { + term.textarea.style.position = 'fixed'; + term.textarea.style.width = '20px'; + term.textarea.style.height = '20px'; + term.textarea.style.left = x - 10 + 'px'; + term.textarea.style.top = y - 10 + 'px'; + term.textarea.style.zIndex = 1000; + term.textarea.focus(); + + // Reset the terminal textarea's styling + setTimeout(function () { + term.textarea.style.position = null; + term.textarea.style.width = null; + term.textarea.style.height = null; + term.textarea.style.left = null; + term.textarea.style.top = null; + term.textarea.style.zIndex = null; + }, 4); + } +} + +exports.prepareTextForClipboard = prepareTextForClipboard; +exports.copyHandler = copyHandler; +exports.pasteHandler = pasteHandler; +exports.rightClickHandler = rightClickHandler; + +},{}],5:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isMSWindows = exports.isIphone = exports.isIpad = exports.isMac = exports.isMSIE = exports.isFirefox = undefined; + +var _Generic = _dereq_('./Generic.js'); + +var isNode = typeof navigator == 'undefined' ? true : false; /** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License) + */ + +/** + * Browser utilities module. This module contains attributes and methods to help with + * identifying the current browser and platform. + * @module xterm/utils/Browser + */ + +var userAgent = isNode ? 'node' : navigator.userAgent; +var platform = isNode ? 'node' : navigator.platform; + +var isFirefox = exports.isFirefox = !!~userAgent.indexOf('Firefox'); +var isMSIE = exports.isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); + +// Find the users platform. We use this to interpret the meta key +// and ISO third level shifts. +// http://stackoverflow.com/q/19877924/577598 +var isMac = exports.isMac = (0, _Generic.contains)(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); +var isIpad = exports.isIpad = platform === 'iPad'; +var isIphone = exports.isIphone = platform === 'iPhone'; +var isMSWindows = exports.isMSWindows = (0, _Generic.contains)(['Windows', 'Win16', 'Win32', 'WinCE'], platform); + +},{"./Generic.js":6}],6:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License) + */ + +/** + * Generic utilities module. This module contains generic methods that can be helpful at + * different parts of the code base. + * @module xterm/utils/Generic + */ + +/** + * Return if the given array contains the given element + * @param {Array} array The array to search for the given element. + * @param {Object} el The element to look for into the array + */ +var contains = exports.contains = function contains(arr, el) { + return arr.indexOf(el) >= 0; +}; + +},{}],7:[function(_dereq_,module,exports){ +'use strict';var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj;}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj;};/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2014, SourceLair Private Company <www.sourcelair.com> (MIT License) + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */var _CompositionHelper=_dereq_('./CompositionHelper.js');var _EventEmitter=_dereq_('./EventEmitter.js');var _Viewport=_dereq_('./Viewport.js');var _Clipboard=_dereq_('./handlers/Clipboard.js');var _Browser=_dereq_('./utils/Browser');var Browser=_interopRequireWildcard(_Browser);function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj;}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key))newObj[key]=obj[key];}}newObj.default=obj;return newObj;}}/** + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */// Let it work inside Node.js for automated testing purposes. +var document=typeof window!='undefined'?window.document:null;/** + * States + */var normal=0,escaped=1,csi=2,osc=3,charset=4,dcs=5,ignore=6;/** + * Terminal + *//** + * Creates a new `Terminal` object. + * + * @param {object} options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */function Terminal(options){var self=this;if(!(this instanceof Terminal)){return new Terminal(arguments[0],arguments[1],arguments[2]);}self.browser=Browser;self.cancel=Terminal.cancel;_EventEmitter.EventEmitter.call(this);if(typeof options==='number'){options={cols:arguments[0],rows:arguments[1],handler:arguments[2]};}options=options||{};Object.keys(Terminal.defaults).forEach(function(key){if(options[key]==null){options[key]=Terminal.options[key];if(Terminal[key]!==Terminal.defaults[key]){options[key]=Terminal[key];}}self[key]=options[key];});if(options.colors.length===8){options.colors=options.colors.concat(Terminal._colors.slice(8));}else if(options.colors.length===16){options.colors=options.colors.concat(Terminal._colors.slice(16));}else if(options.colors.length===10){options.colors=options.colors.slice(0,-2).concat(Terminal._colors.slice(8,-2),options.colors.slice(-2));}else if(options.colors.length===18){options.colors=options.colors.concat(Terminal._colors.slice(16,-2),options.colors.slice(-2));}this.colors=options.colors;this.options=options;// this.context = options.context || window; +// this.document = options.document || document; +this.parent=options.body||options.parent||(document?document.getElementsByTagName('body')[0]:null);this.cols=options.cols||options.geometry[0];this.rows=options.rows||options.geometry[1];this.geometry=[this.cols,this.rows];if(options.handler){this.on('data',options.handler);}/** + * The scroll position of the y cursor, ie. ybase + y = the y position within the entire + * buffer + */this.ybase=0;/** + * The scroll position of the viewport + */this.ydisp=0;/** + * The cursor's x position after ybase + */this.x=0;/** + * The cursor's y position after ybase + */this.y=0;/** + * Used to debounce the refresh function + */this.isRefreshing=false;/** + * Whether there is a full terminal refresh queued + */this.cursorState=0;this.cursorHidden=false;this.convertEol;this.state=0;this.queue='';this.scrollTop=0;this.scrollBottom=this.rows-1;this.customKeydownHandler=null;// modes +this.applicationKeypad=false;this.applicationCursor=false;this.originMode=false;this.insertMode=false;this.wraparoundMode=true;// defaults: xterm - true, vt100 - false +this.normal=null;// charset +this.charset=null;this.gcharset=null;this.glevel=0;this.charsets=[null];// mouse properties +this.decLocator;this.x10Mouse;this.vt200Mouse;this.vt300Mouse;this.normalMouse;this.mouseEvents;this.sendFocus;this.utfMouse;this.sgrMouse;this.urxvtMouse;// misc +this.element;this.children;this.refreshStart;this.refreshEnd;this.savedX;this.savedY;this.savedCols;// stream +this.readable=true;this.writable=true;this.defAttr=0<<18|257<<9|256<<0;this.curAttr=this.defAttr;this.params=[];this.currentParam=0;this.prefix='';this.postfix='';// leftover surrogate high from previous write invocation +this.surrogate_high='';/** + * An array of all lines in the entire buffer, including the prompt. The lines are array of + * characters which are 2-length arrays where [0] is an attribute and [1] is the character. + */this.lines=[];var i=this.rows;while(i--){this.lines.push(this.blankLine());}this.tabs;this.setupStops();// Store if user went browsing history in scrollback +this.userScrolling=false;}inherits(Terminal,_EventEmitter.EventEmitter);/** + * back_color_erase feature for xterm. + */Terminal.prototype.eraseAttr=function(){// if (this.is('screen')) return this.defAttr; +return this.defAttr&~0x1ff|this.curAttr&0x1ff;};/** + * Colors + */// Colors 0-15 +Terminal.tangoColors=[// dark: +'#2e3436','#cc0000','#4e9a06','#c4a000','#3465a4','#75507b','#06989a','#d3d7cf',// bright: +'#555753','#ef2929','#8ae234','#fce94f','#729fcf','#ad7fa8','#34e2e2','#eeeeec'];// Colors 0-15 + 16-255 +// Much thanks to TooTallNate for writing this. +Terminal.colors=function(){var colors=Terminal.tangoColors.slice(),r=[0x00,0x5f,0x87,0xaf,0xd7,0xff],i;// 16-231 +i=0;for(;i<216;i++){out(r[i/36%6|0],r[i/6%6|0],r[i%6]);}// 232-255 (grey) +i=0;for(;i<24;i++){r=8+i*10;out(r,r,r);}function out(r,g,b){colors.push('#'+hex(r)+hex(g)+hex(b));}function hex(c){c=c.toString(16);return c.length<2?'0'+c:c;}return colors;}();Terminal._colors=Terminal.colors.slice();Terminal.vcolors=function(){var out=[],colors=Terminal.colors,i=0,color;for(;i<256;i++){color=parseInt(colors[i].substring(1),16);out.push([color>>16&0xff,color>>8&0xff,color&0xff]);}return out;}();/** + * Options + */Terminal.defaults={colors:Terminal.colors,theme:'default',convertEol:false,termName:'xterm',geometry:[80,24],cursorBlink:false,visualBell:false,popOnBell:false,scrollback:1000,screenKeys:false,debug:false,cancelEvents:false// programFeatures: false, +// focusKeys: false, +};Terminal.options={};Terminal.focus=null;each(keys(Terminal.defaults),function(key){Terminal[key]=Terminal.defaults[key];Terminal.options[key]=Terminal.defaults[key];});/** + * Focus the terminal. Delegates focus handling to the terminal's DOM element. + */Terminal.prototype.focus=function(){return this.textarea.focus();};/** + * Retrieves an option's value from the terminal. + * @param {string} key The option key. + */Terminal.prototype.getOption=function(key,value){if(!(key in Terminal.defaults)){throw new Error('No option with key "'+key+'"');}if(typeof this.options[key]!=='undefined'){return this.options[key];}return this[key];};/** + * Sets an option on the terminal. + * @param {string} key The option key. + * @param {string} value The option value. + */Terminal.prototype.setOption=function(key,value){if(!(key in Terminal.defaults)){throw new Error('No option with key "'+key+'"');}this[key]=value;this.options[key]=value;};/** + * Binds the desired focus behavior on a given terminal object. + * + * @static + */Terminal.bindFocus=function(term){on(term.textarea,'focus',function(ev){if(term.sendFocus){term.send('\x1b[I');}term.element.classList.add('focus');term.showCursor();Terminal.focus=term;term.emit('focus',{terminal:term});});};/** + * Blur the terminal. Delegates blur handling to the terminal's DOM element. + */Terminal.prototype.blur=function(){return this.textarea.blur();};/** + * Binds the desired blur behavior on a given terminal object. + * + * @static + */Terminal.bindBlur=function(term){on(term.textarea,'blur',function(ev){term.refresh(term.y,term.y);if(term.sendFocus){term.send('\x1b[O');}term.element.classList.remove('focus');Terminal.focus=null;term.emit('blur',{terminal:term});});};/** + * Initialize default behavior + */Terminal.prototype.initGlobal=function(){var term=this;Terminal.bindKeys(this);Terminal.bindFocus(this);Terminal.bindBlur(this);// Bind clipboard functionality +on(this.element,'copy',function(ev){_Clipboard.copyHandler.call(this,ev,term);});on(this.textarea,'paste',function(ev){_Clipboard.pasteHandler.call(this,ev,term);});function rightClickHandlerWrapper(ev){_Clipboard.rightClickHandler.call(this,ev,term);}if(term.browser.isFirefox){on(this.element,'mousedown',function(ev){if(ev.button==2){rightClickHandlerWrapper(ev);}});}else{on(this.element,'contextmenu',rightClickHandlerWrapper);}};/** + * Apply key handling to the terminal + */Terminal.bindKeys=function(term){on(term.element,'keydown',function(ev){if(document.activeElement!=this){return;}term.keyDown(ev);},true);on(term.element,'keypress',function(ev){if(document.activeElement!=this){return;}term.keyPress(ev);},true);on(term.element,'keyup',term.focus.bind(term));on(term.textarea,'keydown',function(ev){term.keyDown(ev);},true);on(term.textarea,'keypress',function(ev){term.keyPress(ev);// Truncate the textarea's value, since it is not needed +this.value='';},true);on(term.textarea,'compositionstart',term.compositionHelper.compositionstart.bind(term.compositionHelper));on(term.textarea,'compositionupdate',term.compositionHelper.compositionupdate.bind(term.compositionHelper));on(term.textarea,'compositionend',term.compositionHelper.compositionend.bind(term.compositionHelper));term.on('refresh',term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));};/** + * Insert the given row to the terminal or produce a new one + * if no row argument is passed. Return the inserted row. + * @param {HTMLElement} row (optional) The row to append to the terminal. + */Terminal.prototype.insertRow=function(row){if((typeof row==='undefined'?'undefined':_typeof(row))!='object'){row=document.createElement('div');}this.rowContainer.appendChild(row);this.children.push(row);return row;};/** + * Opens the terminal within an element. + * + * @param {HTMLElement} parent The element to create the terminal within. + */Terminal.prototype.open=function(parent){var self=this,i=0,div;this.parent=parent||this.parent;if(!this.parent){throw new Error('Terminal requires a parent element.');}// Grab global elements +this.context=this.parent.ownerDocument.defaultView;this.document=this.parent.ownerDocument;this.body=this.document.getElementsByTagName('body')[0];//Create main element container +this.element=this.document.createElement('div');this.element.classList.add('terminal');this.element.classList.add('xterm');this.element.classList.add('xterm-theme-'+this.theme);this.element.style.height;this.element.setAttribute('tabindex',0);this.viewportElement=document.createElement('div');this.viewportElement.classList.add('xterm-viewport');this.element.appendChild(this.viewportElement);this.viewportScrollArea=document.createElement('div');this.viewportScrollArea.classList.add('xterm-scroll-area');this.viewportElement.appendChild(this.viewportScrollArea);// Create the container that will hold the lines of the terminal and then +// produce the lines the lines. +this.rowContainer=document.createElement('div');this.rowContainer.classList.add('xterm-rows');this.element.appendChild(this.rowContainer);this.children=[];// Create the container that will hold helpers like the textarea for +// capturing DOM Events. Then produce the helpers. +this.helperContainer=document.createElement('div');this.helperContainer.classList.add('xterm-helpers');// TODO: This should probably be inserted once it's filled to prevent an additional layout +this.element.appendChild(this.helperContainer);this.textarea=document.createElement('textarea');this.textarea.classList.add('xterm-helper-textarea');this.textarea.setAttribute('autocorrect','off');this.textarea.setAttribute('autocapitalize','off');this.textarea.setAttribute('spellcheck','false');this.textarea.tabIndex=0;this.textarea.addEventListener('focus',function(){self.emit('focus',{terminal:self});});this.textarea.addEventListener('blur',function(){self.emit('blur',{terminal:self});});this.helperContainer.appendChild(this.textarea);this.compositionView=document.createElement('div');this.compositionView.classList.add('composition-view');this.compositionHelper=new _CompositionHelper.CompositionHelper(this.textarea,this.compositionView,this);this.helperContainer.appendChild(this.compositionView);this.charMeasureElement=document.createElement('div');this.charMeasureElement.classList.add('xterm-char-measure-element');this.charMeasureElement.innerHTML='W';this.helperContainer.appendChild(this.charMeasureElement);for(;i<this.rows;i++){this.insertRow();}this.parent.appendChild(this.element);this.viewport=new _Viewport.Viewport(this,this.viewportElement,this.viewportScrollArea,this.charMeasureElement);// Draw the screen. +this.refresh(0,this.rows-1);// Initialize global actions that +// need to be taken on the document. +this.initGlobal();// Ensure there is a Terminal.focus. +this.focus();on(this.element,'click',function(){var selection=document.getSelection(),collapsed=selection.isCollapsed,isRange=typeof collapsed=='boolean'?!collapsed:selection.type=='Range';if(!isRange){self.focus();}});// Listen for mouse events and translate +// them into terminal mouse protocols. +this.bindMouse();// Figure out whether boldness affects +// the character width of monospace fonts. +if(Terminal.brokenBold==null){Terminal.brokenBold=isBoldBroken(this.document);}this.emit('open');};/** + * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). + * @param {string} addon The name of the addon to load + * @static + */Terminal.loadAddon=function(addon,callback){if((typeof exports==='undefined'?'undefined':_typeof(exports))==='object'&&(typeof module==='undefined'?'undefined':_typeof(module))==='object'){// CommonJS +return _dereq_('../addons/'+addon);}else if(typeof define=='function'){// RequireJS +return _dereq_(['../addons/'+addon+'/'+addon],callback);}else{console.error('Cannot load a module without a CommonJS or RequireJS environment.');return false;}};/** + * XTerm mouse events + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking + * To better understand these + * the xterm code is very helpful: + * Relevant files: + * button.c, charproc.c, misc.c + * Relevant functions in xterm/button.c: + * BtnCode, EmitButtonCode, EditorButton, SendMousePosition + */Terminal.prototype.bindMouse=function(){var el=this.element,self=this,pressed=32;// mouseup, mousedown, wheel +// left click: ^[[M 3<^[[M#3< +// wheel up: ^[[M`3> +function sendButton(ev){var button,pos;// get the xterm-style button +button=getButton(ev);// get mouse coordinates +pos=getCoords(ev);if(!pos)return;sendEvent(button,pos);switch(ev.overrideType||ev.type){case'mousedown':pressed=button;break;case'mouseup':// keep it at the left +// button, just in case. +pressed=32;break;case'wheel':// nothing. don't +// interfere with +// `pressed`. +break;}}// motion example of a left click: +// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< +function sendMove(ev){var button=pressed,pos;pos=getCoords(ev);if(!pos)return;// buttons marked as motions +// are incremented by 32 +button+=32;sendEvent(button,pos);}// encode button and +// position to characters +function encode(data,ch){if(!self.utfMouse){if(ch===255)return data.push(0);if(ch>127)ch=127;data.push(ch);}else{if(ch===2047)return data.push(0);if(ch<127){data.push(ch);}else{if(ch>2047)ch=2047;data.push(0xC0|ch>>6);data.push(0x80|ch&0x3F);}}}// send a mouse event: +// regular/utf8: ^[[M Cb Cx Cy +// urxvt: ^[[ Cb ; Cx ; Cy M +// sgr: ^[[ Cb ; Cx ; Cy M/m +// vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r +// locator: CSI P e ; P b ; P r ; P c ; P p & w +function sendEvent(button,pos){// self.emit('mouse', { +// x: pos.x - 32, +// y: pos.x - 32, +// button: button +// }); +if(self.vt300Mouse){// NOTE: Unstable. +// http://www.vt100.net/docs/vt3xx-gp/chapter15.html +button&=3;pos.x-=32;pos.y-=32;var data='\x1b[24';if(button===0)data+='1';else if(button===1)data+='3';else if(button===2)data+='5';else if(button===3)return;else data+='0';data+='~['+pos.x+','+pos.y+']\r';self.send(data);return;}if(self.decLocator){// NOTE: Unstable. +button&=3;pos.x-=32;pos.y-=32;if(button===0)button=2;else if(button===1)button=4;else if(button===2)button=6;else if(button===3)button=3;self.send('\x1b['+button+';'+(button===3?4:0)+';'+pos.y+';'+pos.x+';'+(pos.page||0)+'&w');return;}if(self.urxvtMouse){pos.x-=32;pos.y-=32;pos.x++;pos.y++;self.send('\x1b['+button+';'+pos.x+';'+pos.y+'M');return;}if(self.sgrMouse){pos.x-=32;pos.y-=32;self.send('\x1b[<'+((button&3)===3?button&~3:button)+';'+pos.x+';'+pos.y+((button&3)===3?'m':'M'));return;}var data=[];encode(data,button);encode(data,pos.x);encode(data,pos.y);self.send('\x1b[M'+String.fromCharCode.apply(String,data));}function getButton(ev){var button,shift,meta,ctrl,mod;// two low bits: +// 0 = left +// 1 = middle +// 2 = right +// 3 = release +// wheel up/down: +// 1, and 2 - with 64 added +switch(ev.overrideType||ev.type){case'mousedown':button=ev.button!=null?+ev.button:ev.which!=null?ev.which-1:null;if(self.browser.isMSIE){button=button===1?0:button===4?1:button;}break;case'mouseup':button=3;break;case'DOMMouseScroll':button=ev.detail<0?64:65;break;case'wheel':button=ev.wheelDeltaY>0?64:65;break;}// next three bits are the modifiers: +// 4 = shift, 8 = meta, 16 = control +shift=ev.shiftKey?4:0;meta=ev.metaKey?8:0;ctrl=ev.ctrlKey?16:0;mod=shift|meta|ctrl;// no mods +if(self.vt200Mouse){// ctrl only +mod&=ctrl;}else if(!self.normalMouse){mod=0;}// increment to SP +button=32+(mod<<2)+button;return button;}// mouse coordinates measured in cols/rows +function getCoords(ev){var x,y,w,h,el;// ignore browsers without pageX for now +if(ev.pageX==null)return;x=ev.pageX;y=ev.pageY;el=self.element;// should probably check offsetParent +// but this is more portable +while(el&&el!==self.document.documentElement){x-=el.offsetLeft;y-=el.offsetTop;el='offsetParent'in el?el.offsetParent:el.parentNode;}// convert to cols/rows +w=self.element.clientWidth;h=self.element.clientHeight;x=Math.ceil(x/w*self.cols);y=Math.ceil(y/h*self.rows);// be sure to avoid sending +// bad positions to the program +if(x<0)x=0;if(x>self.cols)x=self.cols;if(y<0)y=0;if(y>self.rows)y=self.rows;// xterm sends raw bytes and +// starts at 32 (SP) for each. +x+=32;y+=32;return{x:x,y:y,type:'wheel'};}on(el,'mousedown',function(ev){if(!self.mouseEvents)return;// send the button +sendButton(ev);// ensure focus +self.focus();// fix for odd bug +//if (self.vt200Mouse && !self.normalMouse) { +if(self.vt200Mouse){ev.overrideType='mouseup';sendButton(ev);return self.cancel(ev);}// bind events +if(self.normalMouse)on(self.document,'mousemove',sendMove);// x10 compatibility mode can't send button releases +if(!self.x10Mouse){on(self.document,'mouseup',function up(ev){sendButton(ev);if(self.normalMouse)off(self.document,'mousemove',sendMove);off(self.document,'mouseup',up);return self.cancel(ev);});}return self.cancel(ev);});//if (self.normalMouse) { +// on(self.document, 'mousemove', sendMove); +//} +on(el,'wheel',function(ev){if(!self.mouseEvents)return;if(self.x10Mouse||self.vt300Mouse||self.decLocator)return;sendButton(ev);return self.cancel(ev);});// allow wheel scrolling in +// the shell for example +on(el,'wheel',function(ev){if(self.mouseEvents)return;self.viewport.onWheel(ev);return self.cancel(ev);});};/** + * Destroys the terminal. + */Terminal.prototype.destroy=function(){this.readable=false;this.writable=false;this._events={};this.handler=function(){};this.write=function(){};if(this.element.parentNode){this.element.parentNode.removeChild(this.element);}//this.emit('close'); +};/** + * Flags used to render terminal text properly + */Terminal.flags={BOLD:1,UNDERLINE:2,BLINK:4,INVERSE:8,INVISIBLE:16};/** + * Refreshes (re-renders) terminal content within two rows (inclusive) + * + * Rendering Engine: + * + * In the screen buffer, each character is stored as a an array with a character + * and a 32-bit integer: + * - First value: a utf-16 character. + * - Second value: + * - Next 9 bits: background color (0-511). + * - Next 9 bits: foreground color (0-511). + * - Next 14 bits: a mask for misc. flags: + * - 1=bold + * - 2=underline + * - 4=blink + * - 8=inverse + * - 16=invisible + * + * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) + * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) + * @param {boolean} queue Whether the refresh should ran right now or be queued + */Terminal.prototype.refresh=function(start,end,queue){var self=this;// queue defaults to true +queue=typeof queue=='undefined'?true:queue;/** + * The refresh queue allows refresh to execute only approximately 30 times a second. For + * commands that pass a significant amount of output to the write function, this prevents the + * terminal from maxing out the CPU and making the UI unresponsive. While commands can still + * run beyond what they do on the terminal, it is far better with a debounce in place as + * every single terminal manipulation does not need to be constructed in the DOM. + * + * A side-effect of this is that it makes ^C to interrupt a process seem more responsive. + */if(queue){// If refresh should be queued, order the refresh and return. +if(this._refreshIsQueued){// If a refresh has already been queued, just order a full refresh next +this._fullRefreshNext=true;}else{setTimeout(function(){self.refresh(start,end,false);},34);this._refreshIsQueued=true;}return;}// If refresh should be run right now (not be queued), release the lock +this._refreshIsQueued=false;// If multiple refreshes were requested, make a full refresh. +if(this._fullRefreshNext){start=0;end=this.rows-1;this._fullRefreshNext=false;// reset lock +}var x,y,i,line,out,ch,ch_width,width,data,attr,bg,fg,flags,row,parent,focused=document.activeElement;// If this is a big refresh, remove the terminal rows from the DOM for faster calculations +if(end-start>=this.rows/2){parent=this.element.parentNode;if(parent){this.element.removeChild(this.rowContainer);}}width=this.cols;y=start;if(end>=this.rows.length){this.log('`end` is too large. Most likely a bad CSR.');end=this.rows.length-1;}for(;y<=end;y++){row=y+this.ydisp;line=this.lines[row];out='';if(this.y===y-(this.ybase-this.ydisp)&&this.cursorState&&!this.cursorHidden){x=this.x;}else{x=-1;}attr=this.defAttr;i=0;for(;i<width;i++){data=line[i][0];ch=line[i][1];ch_width=line[i][2];if(!ch_width)continue;if(i===x)data=-1;if(data!==attr){if(attr!==this.defAttr){out+='</span>';}if(data!==this.defAttr){if(data===-1){out+='<span class="reverse-video terminal-cursor';if(this.cursorBlink){out+=' blinking';}out+='">';}else{var classNames=[];bg=data&0x1ff;fg=data>>9&0x1ff;flags=data>>18;if(flags&Terminal.flags.BOLD){if(!Terminal.brokenBold){classNames.push('xterm-bold');}// See: XTerm*boldColors +if(fg<8)fg+=8;}if(flags&Terminal.flags.UNDERLINE){classNames.push('xterm-underline');}if(flags&Terminal.flags.BLINK){classNames.push('xterm-blink');}// If inverse flag is on, then swap the foreground and background variables. +if(flags&Terminal.flags.INVERSE){/* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */bg=[fg,fg=bg][0];// Should inverse just be before the +// above boldColors effect instead? +if(flags&1&&fg<8)fg+=8;}if(flags&Terminal.flags.INVISIBLE){classNames.push('xterm-hidden');}/** + * Weird situation: Invert flag used black foreground and white background results + * in invalid background color, positioned at the 256 index of the 256 terminal + * color map. Pin the colors manually in such a case. + * + * Source: https://github.com/sourcelair/xterm.js/issues/57 + */if(flags&Terminal.flags.INVERSE){if(bg==257){bg=15;}if(fg==256){fg=0;}}if(bg<256){classNames.push('xterm-bg-color-'+bg);}if(fg<256){classNames.push('xterm-color-'+fg);}out+='<span';if(classNames.length){out+=' class="'+classNames.join(' ')+'"';}out+='>';}}}switch(ch){case'&':out+='&';break;case'<':out+='<';break;case'>':out+='>';break;default:if(ch<=' '){out+=' ';}else{out+=ch;}break;}attr=data;}if(attr!==this.defAttr){out+='</span>';}this.children[y].innerHTML=out;}if(parent){this.element.appendChild(this.rowContainer);}this.emit('refresh',{element:this.element,start:start,end:end});};/** + * Display the cursor element + */Terminal.prototype.showCursor=function(){if(!this.cursorState){this.cursorState=1;this.refresh(this.y,this.y);}};/** + * Scroll the terminal + */Terminal.prototype.scroll=function(){var row;if(++this.ybase===this.scrollback){this.ybase=this.ybase/2|0;this.lines=this.lines.slice(-(this.ybase+this.rows)+1);}if(!this.userScrolling){this.ydisp=this.ybase;}// last line +row=this.ybase+this.rows-1;// subtract the bottom scroll region +row-=this.rows-1-this.scrollBottom;if(row===this.lines.length){// potential optimization: +// pushing is faster than splicing +// when they amount to the same +// behavior. +this.lines.push(this.blankLine());}else{// add our new line +this.lines.splice(row,0,this.blankLine());}if(this.scrollTop!==0){if(this.ybase!==0){this.ybase--;if(!this.userScrolling){this.ydisp=this.ybase;}}this.lines.splice(this.ybase+this.scrollTop,1);}// this.maxRange(); +this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);this.emit('scroll',this.ydisp);};/** + * Scroll the display of the terminal + * @param {number} disp The number of lines to scroll down (negatives scroll up). + * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used + * to avoid unwanted events being handled by the veiwport when the event was triggered from the + * viewport originally. + */Terminal.prototype.scrollDisp=function(disp,suppressScrollEvent){if(disp<0){this.userScrolling=true;}else if(disp+this.ydisp>=this.ybase){this.userScrolling=false;}this.ydisp+=disp;if(this.ydisp>this.ybase){this.ydisp=this.ybase;}else if(this.ydisp<0){this.ydisp=0;}if(!suppressScrollEvent){this.emit('scroll',this.ydisp);}this.refresh(0,this.rows-1);};/** + * Scroll the display of the terminal by a number of pages. + * @param {number} pageCount The number of pages to scroll (negative scrolls up). + */Terminal.prototype.scrollPages=function(pageCount){this.scrollDisp(pageCount*(this.rows-1));};/** + * Scrolls the display of the terminal to the top. + */Terminal.prototype.scrollToTop=function(){this.scrollDisp(-this.ydisp);};/** + * Scrolls the display of the terminal to the bottom. + */Terminal.prototype.scrollToBottom=function(){this.scrollDisp(this.ybase-this.ydisp);};/** + * Writes text to the terminal. + * @param {string} text The text to write to the terminal. + */Terminal.prototype.write=function(data){var l=data.length,i=0,j,cs,ch,code,low,ch_width,row;this.refreshStart=this.y;this.refreshEnd=this.y;// apply leftover surrogate high from last write +if(this.surrogate_high){data=this.surrogate_high+data;this.surrogate_high='';}for(;i<l;i++){ch=data[i];// FIXME: higher chars than 0xa0 are not allowed in escape sequences +// --> maybe move to default +code=data.charCodeAt(i);if(0xD800<=code&&code<=0xDBFF){// we got a surrogate high +// get surrogate low (next 2 bytes) +low=data.charCodeAt(i+1);if(isNaN(low)){// end of data stream, save surrogate high +this.surrogate_high=ch;continue;}code=(code-0xD800)*0x400+(low-0xDC00)+0x10000;ch+=data.charAt(i+1);}// surrogate low - already handled above +if(0xDC00<=code&&code<=0xDFFF)continue;switch(this.state){case normal:switch(ch){case'\x07':this.bell();break;// '\n', '\v', '\f' +case'\n':case'\x0b':case'\x0c':if(this.convertEol){this.x=0;}this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}break;// '\r' +case'\r':this.x=0;break;// '\b' +case'\x08':if(this.x>0){this.x--;}break;// '\t' +case'\t':this.x=this.nextStop();break;// shift out +case'\x0e':this.setgLevel(1);break;// shift in +case'\x0f':this.setgLevel(0);break;// '\e' +case'\x1b':this.state=escaped;break;default:// ' ' +// calculate print space +// expensive call, therefore we save width in line buffer +ch_width=wcwidth(code);if(ch>=' '){if(this.charset&&this.charset[ch]){ch=this.charset[ch];}row=this.y+this.ybase;// insert combining char in last cell +// FIXME: needs handling after cursor jumps +if(!ch_width&&this.x){// dont overflow left +if(this.lines[row][this.x-1]){if(!this.lines[row][this.x-1][2]){// found empty cell after fullwidth, need to go 2 cells back +if(this.lines[row][this.x-2])this.lines[row][this.x-2][1]+=ch;}else{this.lines[row][this.x-1][1]+=ch;}this.updateRange(this.y);}break;}// goto next line if ch would overflow +// TODO: needs a global min terminal width of 2 +if(this.x+ch_width-1>=this.cols){// autowrap - DECAWM +if(this.wraparoundMode){this.x=0;this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}}else{this.x=this.cols-1;if(ch_width===2)// FIXME: check for xterm behavior +continue;}}row=this.y+this.ybase;// insert mode: move characters to right +if(this.insertMode){// do this twice for a fullwidth char +for(var moves=0;moves<ch_width;++moves){// remove last cell, if it's width is 0 +// we have to adjust the second last cell as well +var removed=this.lines[this.y+this.ybase].pop();if(removed[2]===0&&this.lines[row][this.cols-2]&&this.lines[row][this.cols-2][2]===2)this.lines[row][this.cols-2]=[this.curAttr,' ',1];// insert empty cell at cursor +this.lines[row].splice(this.x,0,[this.curAttr,' ',1]);}}this.lines[row][this.x]=[this.curAttr,ch,ch_width];this.x++;this.updateRange(this.y);// fullwidth char - set next cell width to zero and advance cursor +if(ch_width===2){this.lines[row][this.x]=[this.curAttr,'',0];this.x++;}}break;}break;case escaped:switch(ch){// ESC [ Control Sequence Introducer ( CSI is 0x9b). +case'[':this.params=[];this.currentParam=0;this.state=csi;break;// ESC ] Operating System Command ( OSC is 0x9d). +case']':this.params=[];this.currentParam=0;this.state=osc;break;// ESC P Device Control String ( DCS is 0x90). +case'P':this.params=[];this.currentParam=0;this.state=dcs;break;// ESC _ Application Program Command ( APC is 0x9f). +case'_':this.state=ignore;break;// ESC ^ Privacy Message ( PM is 0x9e). +case'^':this.state=ignore;break;// ESC c Full Reset (RIS). +case'c':this.reset();break;// ESC E Next Line ( NEL is 0x85). +// ESC D Index ( IND is 0x84). +case'E':this.x=0;;case'D':this.index();break;// ESC M Reverse Index ( RI is 0x8d). +case'M':this.reverseIndex();break;// ESC % Select default/utf-8 character set. +// @ = default, G = utf-8 +case'%'://this.charset = null; +this.setgLevel(0);this.setgCharset(0,Terminal.charsets.US);this.state=normal;i++;break;// ESC (,),*,+,-,. Designate G0-G2 Character Set. +case'(':// <-- this seems to get all the attention +case')':case'*':case'+':case'-':case'.':switch(ch){case'(':this.gcharset=0;break;case')':this.gcharset=1;break;case'*':this.gcharset=2;break;case'+':this.gcharset=3;break;case'-':this.gcharset=1;break;case'.':this.gcharset=2;break;}this.state=charset;break;// Designate G3 Character Set (VT300). +// A = ISO Latin-1 Supplemental. +// Not implemented. +case'/':this.gcharset=3;this.state=charset;i--;break;// ESC N +// Single Shift Select of G2 Character Set +// ( SS2 is 0x8e). This affects next character only. +case'N':break;// ESC O +// Single Shift Select of G3 Character Set +// ( SS3 is 0x8f). This affects next character only. +case'O':break;// ESC n +// Invoke the G2 Character Set as GL (LS2). +case'n':this.setgLevel(2);break;// ESC o +// Invoke the G3 Character Set as GL (LS3). +case'o':this.setgLevel(3);break;// ESC | +// Invoke the G3 Character Set as GR (LS3R). +case'|':this.setgLevel(3);break;// ESC } +// Invoke the G2 Character Set as GR (LS2R). +case'}':this.setgLevel(2);break;// ESC ~ +// Invoke the G1 Character Set as GR (LS1R). +case'~':this.setgLevel(1);break;// ESC 7 Save Cursor (DECSC). +case'7':this.saveCursor();this.state=normal;break;// ESC 8 Restore Cursor (DECRC). +case'8':this.restoreCursor();this.state=normal;break;// ESC # 3 DEC line height/width +case'#':this.state=normal;i++;break;// ESC H Tab Set (HTS is 0x88). +case'H':this.tabSet();break;// ESC = Application Keypad (DECKPAM). +case'=':this.log('Serial port requested application keypad.');this.applicationKeypad=true;this.viewport.syncScrollArea();this.state=normal;break;// ESC > Normal Keypad (DECKPNM). +case'>':this.log('Switching back to normal keypad.');this.applicationKeypad=false;this.viewport.syncScrollArea();this.state=normal;break;default:this.state=normal;this.error('Unknown ESC control: %s.',ch);break;}break;case charset:switch(ch){case'0':// DEC Special Character and Line Drawing Set. +cs=Terminal.charsets.SCLD;break;case'A':// UK +cs=Terminal.charsets.UK;break;case'B':// United States (USASCII). +cs=Terminal.charsets.US;break;case'4':// Dutch +cs=Terminal.charsets.Dutch;break;case'C':// Finnish +case'5':cs=Terminal.charsets.Finnish;break;case'R':// French +cs=Terminal.charsets.French;break;case'Q':// FrenchCanadian +cs=Terminal.charsets.FrenchCanadian;break;case'K':// German +cs=Terminal.charsets.German;break;case'Y':// Italian +cs=Terminal.charsets.Italian;break;case'E':// NorwegianDanish +case'6':cs=Terminal.charsets.NorwegianDanish;break;case'Z':// Spanish +cs=Terminal.charsets.Spanish;break;case'H':// Swedish +case'7':cs=Terminal.charsets.Swedish;break;case'=':// Swiss +cs=Terminal.charsets.Swiss;break;case'/':// ISOLatin (actually /A) +cs=Terminal.charsets.ISOLatin;i++;break;default:// Default +cs=Terminal.charsets.US;break;}this.setgCharset(this.gcharset,cs);this.gcharset=null;this.state=normal;break;case osc:// OSC Ps ; Pt ST +// OSC Ps ; Pt BEL +// Set Text Parameters. +if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;this.params.push(this.currentParam);switch(this.params[0]){case 0:case 1:case 2:if(this.params[1]){this.title=this.params[1];this.handleTitle(this.title);}break;case 3:// set X property +break;case 4:case 5:// change dynamic colors +break;case 10:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:// change dynamic ui colors +break;case 46:// change log file +break;case 50:// dynamic font +break;case 51:// emacs shell +break;case 52:// manipulate selection data +break;case 104:case 105:case 110:case 111:case 112:case 113:case 114:case 115:case 116:case 117:case 118:// reset colors +break;}this.params=[];this.currentParam=0;this.state=normal;}else{if(!this.params.length){if(ch>='0'&&ch<='9'){this.currentParam=this.currentParam*10+ch.charCodeAt(0)-48;}else if(ch===';'){this.params.push(this.currentParam);this.currentParam='';}}else{this.currentParam+=ch;}}break;case csi:// '?', '>', '!' +if(ch==='?'||ch==='>'||ch==='!'){this.prefix=ch;break;}// 0 - 9 +if(ch>='0'&&ch<='9'){this.currentParam=this.currentParam*10+ch.charCodeAt(0)-48;break;}// '$', '"', ' ', '\'' +if(ch==='$'||ch==='"'||ch===' '||ch==='\''){this.postfix=ch;break;}this.params.push(this.currentParam);this.currentParam=0;// ';' +if(ch===';')break;this.state=normal;switch(ch){// CSI Ps A +// Cursor Up Ps Times (default = 1) (CUU). +case'A':this.cursorUp(this.params);break;// CSI Ps B +// Cursor Down Ps Times (default = 1) (CUD). +case'B':this.cursorDown(this.params);break;// CSI Ps C +// Cursor Forward Ps Times (default = 1) (CUF). +case'C':this.cursorForward(this.params);break;// CSI Ps D +// Cursor Backward Ps Times (default = 1) (CUB). +case'D':this.cursorBackward(this.params);break;// CSI Ps ; Ps H +// Cursor Position [row;column] (default = [1,1]) (CUP). +case'H':this.cursorPos(this.params);break;// CSI Ps J Erase in Display (ED). +case'J':this.eraseInDisplay(this.params);break;// CSI Ps K Erase in Line (EL). +case'K':this.eraseInLine(this.params);break;// CSI Pm m Character Attributes (SGR). +case'm':if(!this.prefix){this.charAttributes(this.params);}break;// CSI Ps n Device Status Report (DSR). +case'n':if(!this.prefix){this.deviceStatus(this.params);}break;/** + * Additions + */// CSI Ps @ +// Insert Ps (Blank) Character(s) (default = 1) (ICH). +case'@':this.insertChars(this.params);break;// CSI Ps E +// Cursor Next Line Ps Times (default = 1) (CNL). +case'E':this.cursorNextLine(this.params);break;// CSI Ps F +// Cursor Preceding Line Ps Times (default = 1) (CNL). +case'F':this.cursorPrecedingLine(this.params);break;// CSI Ps G +// Cursor Character Absolute [column] (default = [row,1]) (CHA). +case'G':this.cursorCharAbsolute(this.params);break;// CSI Ps L +// Insert Ps Line(s) (default = 1) (IL). +case'L':this.insertLines(this.params);break;// CSI Ps M +// Delete Ps Line(s) (default = 1) (DL). +case'M':this.deleteLines(this.params);break;// CSI Ps P +// Delete Ps Character(s) (default = 1) (DCH). +case'P':this.deleteChars(this.params);break;// CSI Ps X +// Erase Ps Character(s) (default = 1) (ECH). +case'X':this.eraseChars(this.params);break;// CSI Pm ` Character Position Absolute +// [column] (default = [row,1]) (HPA). +case'`':this.charPosAbsolute(this.params);break;// 141 61 a * HPR - +// Horizontal Position Relative +case'a':this.HPositionRelative(this.params);break;// CSI P s c +// Send Device Attributes (Primary DA). +// CSI > P s c +// Send Device Attributes (Secondary DA) +case'c':this.sendDeviceAttributes(this.params);break;// CSI Pm d +// Line Position Absolute [row] (default = [1,column]) (VPA). +case'd':this.linePosAbsolute(this.params);break;// 145 65 e * VPR - Vertical Position Relative +case'e':this.VPositionRelative(this.params);break;// CSI Ps ; Ps f +// Horizontal and Vertical Position [row;column] (default = +// [1,1]) (HVP). +case'f':this.HVPosition(this.params);break;// CSI Pm h Set Mode (SM). +// CSI ? Pm h - mouse escape codes, cursor escape codes +case'h':this.setMode(this.params);break;// CSI Pm l Reset Mode (RM). +// CSI ? Pm l +case'l':this.resetMode(this.params);break;// CSI Ps ; Ps r +// Set Scrolling Region [top;bottom] (default = full size of win- +// dow) (DECSTBM). +// CSI ? Pm r +case'r':this.setScrollRegion(this.params);break;// CSI s +// Save cursor (ANSI.SYS). +case's':this.saveCursor(this.params);break;// CSI u +// Restore cursor (ANSI.SYS). +case'u':this.restoreCursor(this.params);break;/** + * Lesser Used + */// CSI Ps I +// Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). +case'I':this.cursorForwardTab(this.params);break;// CSI Ps S Scroll up Ps lines (default = 1) (SU). +case'S':this.scrollUp(this.params);break;// CSI Ps T Scroll down Ps lines (default = 1) (SD). +// CSI Ps ; Ps ; Ps ; Ps ; Ps T +// CSI > Ps; Ps T +case'T':// if (this.prefix === '>') { +// this.resetTitleModes(this.params); +// break; +// } +// if (this.params.length > 2) { +// this.initMouseTracking(this.params); +// break; +// } +if(this.params.length<2&&!this.prefix){this.scrollDown(this.params);}break;// CSI Ps Z +// Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). +case'Z':this.cursorBackwardTab(this.params);break;// CSI Ps b Repeat the preceding graphic character Ps times (REP). +case'b':this.repeatPrecedingCharacter(this.params);break;// CSI Ps g Tab Clear (TBC). +case'g':this.tabClear(this.params);break;// CSI Pm i Media Copy (MC). +// CSI ? Pm i +// case 'i': +// this.mediaCopy(this.params); +// break; +// CSI Pm m Character Attributes (SGR). +// CSI > Ps; Ps m +// case 'm': // duplicate +// if (this.prefix === '>') { +// this.setResources(this.params); +// } else { +// this.charAttributes(this.params); +// } +// break; +// CSI Ps n Device Status Report (DSR). +// CSI > Ps n +// case 'n': // duplicate +// if (this.prefix === '>') { +// this.disableModifiers(this.params); +// } else { +// this.deviceStatus(this.params); +// } +// break; +// CSI > Ps p Set pointer mode. +// CSI ! p Soft terminal reset (DECSTR). +// CSI Ps$ p +// Request ANSI mode (DECRQM). +// CSI ? Ps$ p +// Request DEC private mode (DECRQM). +// CSI Ps ; Ps " p +case'p':switch(this.prefix){// case '>': +// this.setPointerMode(this.params); +// break; +case'!':this.softReset(this.params);break;// case '?': +// if (this.postfix === '$') { +// this.requestPrivateMode(this.params); +// } +// break; +// default: +// if (this.postfix === '"') { +// this.setConformanceLevel(this.params); +// } else if (this.postfix === '$') { +// this.requestAnsiMode(this.params); +// } +// break; +}break;// CSI Ps q Load LEDs (DECLL). +// CSI Ps SP q +// CSI Ps " q +// case 'q': +// if (this.postfix === ' ') { +// this.setCursorStyle(this.params); +// break; +// } +// if (this.postfix === '"') { +// this.setCharProtectionAttr(this.params); +// break; +// } +// this.loadLEDs(this.params); +// break; +// CSI Ps ; Ps r +// Set Scrolling Region [top;bottom] (default = full size of win- +// dow) (DECSTBM). +// CSI ? Pm r +// CSI Pt; Pl; Pb; Pr; Ps$ r +// case 'r': // duplicate +// if (this.prefix === '?') { +// this.restorePrivateValues(this.params); +// } else if (this.postfix === '$') { +// this.setAttrInRectangle(this.params); +// } else { +// this.setScrollRegion(this.params); +// } +// break; +// CSI s Save cursor (ANSI.SYS). +// CSI ? Pm s +// case 's': // duplicate +// if (this.prefix === '?') { +// this.savePrivateValues(this.params); +// } else { +// this.saveCursor(this.params); +// } +// break; +// CSI Ps ; Ps ; Ps t +// CSI Pt; Pl; Pb; Pr; Ps$ t +// CSI > Ps; Ps t +// CSI Ps SP t +// case 't': +// if (this.postfix === '$') { +// this.reverseAttrInRectangle(this.params); +// } else if (this.postfix === ' ') { +// this.setWarningBellVolume(this.params); +// } else { +// if (this.prefix === '>') { +// this.setTitleModeFeature(this.params); +// } else { +// this.manipulateWindow(this.params); +// } +// } +// break; +// CSI u Restore cursor (ANSI.SYS). +// CSI Ps SP u +// case 'u': // duplicate +// if (this.postfix === ' ') { +// this.setMarginBellVolume(this.params); +// } else { +// this.restoreCursor(this.params); +// } +// break; +// CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v +// case 'v': +// if (this.postfix === '$') { +// this.copyRectagle(this.params); +// } +// break; +// CSI Pt ; Pl ; Pb ; Pr ' w +// case 'w': +// if (this.postfix === '\'') { +// this.enableFilterRectangle(this.params); +// } +// break; +// CSI Ps x Request Terminal Parameters (DECREQTPARM). +// CSI Ps x Select Attribute Change Extent (DECSACE). +// CSI Pc; Pt; Pl; Pb; Pr$ x +// case 'x': +// if (this.postfix === '$') { +// this.fillRectangle(this.params); +// } else { +// this.requestParameters(this.params); +// //this.__(this.params); +// } +// break; +// CSI Ps ; Pu ' z +// CSI Pt; Pl; Pb; Pr$ z +// case 'z': +// if (this.postfix === '\'') { +// this.enableLocatorReporting(this.params); +// } else if (this.postfix === '$') { +// this.eraseRectangle(this.params); +// } +// break; +// CSI Pm ' { +// CSI Pt; Pl; Pb; Pr$ { +// case '{': +// if (this.postfix === '\'') { +// this.setLocatorEvents(this.params); +// } else if (this.postfix === '$') { +// this.selectiveEraseRectangle(this.params); +// } +// break; +// CSI Ps ' | +// case '|': +// if (this.postfix === '\'') { +// this.requestLocatorPosition(this.params); +// } +// break; +// CSI P m SP } +// Insert P s Column(s) (default = 1) (DECIC), VT420 and up. +// case '}': +// if (this.postfix === ' ') { +// this.insertColumns(this.params); +// } +// break; +// CSI P m SP ~ +// Delete P s Column(s) (default = 1) (DECDC), VT420 and up +// case '~': +// if (this.postfix === ' ') { +// this.deleteColumns(this.params); +// } +// break; +default:this.error('Unknown CSI code: %s.',ch);break;}this.prefix='';this.postfix='';break;case dcs:if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;switch(this.prefix){// User-Defined Keys (DECUDK). +case'':break;// Request Status String (DECRQSS). +// test: echo -e '\eP$q"p\e\\' +case'$q':var pt=this.currentParam,valid=false;switch(pt){// DECSCA +case'"q':pt='0"q';break;// DECSCL +case'"p':pt='61"p';break;// DECSTBM +case'r':pt=''+(this.scrollTop+1)+';'+(this.scrollBottom+1)+'r';break;// SGR +case'm':pt='0m';break;default:this.error('Unknown DCS Pt: %s.',pt);pt='';break;}this.send('\x1bP'+ +valid+'$r'+pt+'\x1b\\');break;// Set Termcap/Terminfo Data (xterm, experimental). +case'+p':break;// Request Termcap/Terminfo String (xterm, experimental) +// Regular xterm does not even respond to this sequence. +// This can cause a small glitch in vim. +// test: echo -ne '\eP+q6b64\e\\' +case'+q':var pt=this.currentParam,valid=false;this.send('\x1bP'+ +valid+'+r'+pt+'\x1b\\');break;default:this.error('Unknown DCS prefix: %s.',this.prefix);break;}this.currentParam=0;this.prefix='';this.state=normal;}else if(!this.currentParam){if(!this.prefix&&ch!=='$'&&ch!=='+'){this.currentParam=ch;}else if(this.prefix.length===2){this.currentParam=ch;}else{this.prefix+=ch;}}else{this.currentParam+=ch;}break;case ignore:// For PM and APC. +if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;this.state=normal;}break;}}this.updateRange(this.y);this.refresh(this.refreshStart,this.refreshEnd);};/** + * Writes text to the terminal, followed by a break line character (\n). + * @param {string} text The text to write to the terminal. + */Terminal.prototype.writeln=function(data){this.write(data+'\r\n');};/** + * Attaches a custom keydown handler which is run before keys are processed, giving consumers of + * xterm.js ultimate control as to what keys should be processed by the terminal and what keys + * should not. + * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a + * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent + * the default action. The function returns whether the event should be processed by xterm.js. + */Terminal.prototype.attachCustomKeydownHandler=function(customKeydownHandler){this.customKeydownHandler=customKeydownHandler;};/** + * Handle a keydown event + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param {KeyboardEvent} ev The keydown event to be handled. + */Terminal.prototype.keyDown=function(ev){// Scroll down to prompt, whenever the user presses a key. +if(this.ybase!==this.ydisp){this.scrollToBottom();}if(this.customKeydownHandler&&this.customKeydownHandler(ev)===false){return false;}if(!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)){return false;}var self=this;var result=this.evaluateKeyEscapeSequence(ev);if(result.scrollDisp){this.scrollDisp(result.scrollDisp);return this.cancel(ev,true);}if(isThirdLevelShift(this,ev)){return true;}if(result.cancel){// The event is canceled at the end already, is this necessary? +this.cancel(ev,true);}if(!result.key){return true;}this.emit('keydown',ev);this.emit('key',result.key,ev);this.showCursor();this.handler(result.key);return this.cancel(ev,true);};/** + * Returns an object that determines how a KeyboardEvent should be handled. The key of the + * returned value is the new key code to pass to the PTY. + * + * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. + */Terminal.prototype.evaluateKeyEscapeSequence=function(ev){var result={// Whether to cancel event propogation (NOTE: this may not be needed since the event is +// canceled at the end of keyDown +cancel:false,// The new key even to emit +key:undefined,// The number of characters to scroll, if this is defined it will cancel the event +scrollDisp:undefined};var modifiers=ev.shiftKey<<0|ev.altKey<<1|ev.ctrlKey<<2|ev.metaKey<<3;switch(ev.keyCode){case 8:// backspace +if(ev.shiftKey){result.key='\x08';// ^H +break;}result.key='\x7f';// ^? +break;case 9:// tab +if(ev.shiftKey){result.key='\x1b[Z';break;}result.key='\t';result.cancel=true;break;case 13:// return/enter +result.key='\r';result.cancel=true;break;case 27:// escape +result.key='\x1b';result.cancel=true;break;case 37:// left-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'D';// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3D'){result.key='\x1b[1;5D';}}else if(this.applicationCursor){result.key='\x1bOD';}else{result.key='\x1b[D';}break;case 39:// right-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'C';// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3C'){result.key='\x1b[1;5C';}}else if(this.applicationCursor){result.key='\x1bOC';}else{result.key='\x1b[C';}break;case 38:// up-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'A';// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3A'){result.key='\x1b[1;5A';}}else if(this.applicationCursor){result.key='\x1bOA';}else{result.key='\x1b[A';}break;case 40:// down-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'B';// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3B'){result.key='\x1b[1;5B';}}else if(this.applicationCursor){result.key='\x1bOB';}else{result.key='\x1b[B';}break;case 45:// insert +if(!ev.shiftKey&&!ev.ctrlKey){// <Ctrl> or <Shift> + <Insert> are used to +// copy-paste on some systems. +result.key='\x1b[2~';}break;case 46:// delete +if(modifiers){result.key='\x1b[3;'+(modifiers+1)+'~';}else{result.key='\x1b[3~';}break;case 36:// home +if(modifiers)result.key='\x1b[1;'+(modifiers+1)+'H';else if(this.applicationCursor)result.key='\x1bOH';else result.key='\x1b[H';break;case 35:// end +if(modifiers)result.key='\x1b[1;'+(modifiers+1)+'F';else if(this.applicationCursor)result.key='\x1bOF';else result.key='\x1b[F';break;case 33:// page up +if(ev.shiftKey){result.scrollDisp=-(this.rows-1);}else{result.key='\x1b[5~';}break;case 34:// page down +if(ev.shiftKey){result.scrollDisp=this.rows-1;}else{result.key='\x1b[6~';}break;case 112:// F1-F12 +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'P';}else{result.key='\x1bOP';}break;case 113:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'Q';}else{result.key='\x1bOQ';}break;case 114:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'R';}else{result.key='\x1bOR';}break;case 115:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'S';}else{result.key='\x1bOS';}break;case 116:if(modifiers){result.key='\x1b[15;'+(modifiers+1)+'~';}else{result.key='\x1b[15~';}break;case 117:if(modifiers){result.key='\x1b[17;'+(modifiers+1)+'~';}else{result.key='\x1b[17~';}break;case 118:if(modifiers){result.key='\x1b[18;'+(modifiers+1)+'~';}else{result.key='\x1b[18~';}break;case 119:if(modifiers){result.key='\x1b[19;'+(modifiers+1)+'~';}else{result.key='\x1b[19~';}break;case 120:if(modifiers){result.key='\x1b[20;'+(modifiers+1)+'~';}else{result.key='\x1b[20~';}break;case 121:if(modifiers){result.key='\x1b[21;'+(modifiers+1)+'~';}else{result.key='\x1b[21~';}break;case 122:if(modifiers){result.key='\x1b[23;'+(modifiers+1)+'~';}else{result.key='\x1b[23~';}break;case 123:if(modifiers){result.key='\x1b[24;'+(modifiers+1)+'~';}else{result.key='\x1b[24~';}break;default:// a-z and space +if(ev.ctrlKey&&!ev.shiftKey&&!ev.altKey&&!ev.metaKey){if(ev.keyCode>=65&&ev.keyCode<=90){result.key=String.fromCharCode(ev.keyCode-64);}else if(ev.keyCode===32){// NUL +result.key=String.fromCharCode(0);}else if(ev.keyCode>=51&&ev.keyCode<=55){// escape, file sep, group sep, record sep, unit sep +result.key=String.fromCharCode(ev.keyCode-51+27);}else if(ev.keyCode===56){// delete +result.key=String.fromCharCode(127);}else if(ev.keyCode===219){// ^[ - escape +result.key=String.fromCharCode(27);}else if(ev.keyCode===221){// ^] - group sep +result.key=String.fromCharCode(29);}}else if(!this.browser.isMac&&ev.altKey&&!ev.ctrlKey&&!ev.metaKey){// On Mac this is a third level shift. Use <Esc> instead. +if(ev.keyCode>=65&&ev.keyCode<=90){result.key='\x1b'+String.fromCharCode(ev.keyCode+32);}else if(ev.keyCode===192){result.key='\x1b`';}else if(ev.keyCode>=48&&ev.keyCode<=57){result.key='\x1b'+(ev.keyCode-48);}}break;}return result;};/** + * Set the G level of the terminal + * @param g + */Terminal.prototype.setgLevel=function(g){this.glevel=g;this.charset=this.charsets[g];};/** + * Set the charset for the given G level of the terminal + * @param g + * @param charset + */Terminal.prototype.setgCharset=function(g,charset){this.charsets[g]=charset;if(this.glevel===g){this.charset=charset;}};/** + * Handle a keypress event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param {KeyboardEvent} ev The keypress event to be handled. + */Terminal.prototype.keyPress=function(ev){var key;this.cancel(ev);if(ev.charCode){key=ev.charCode;}else if(ev.which==null){key=ev.keyCode;}else if(ev.which!==0&&ev.charCode!==0){key=ev.which;}else{return false;}if(!key||(ev.altKey||ev.ctrlKey||ev.metaKey)&&!isThirdLevelShift(this,ev)){return false;}key=String.fromCharCode(key);this.emit('keypress',key,ev);this.emit('key',key,ev);this.showCursor();this.handler(key);return false;};/** + * Send data for handling to the terminal + * @param {string} data + */Terminal.prototype.send=function(data){var self=this;if(!this.queue){setTimeout(function(){self.handler(self.queue);self.queue='';},1);}this.queue+=data;};/** + * Ring the bell. + * Note: We could do sweet things with webaudio here + */Terminal.prototype.bell=function(){if(!this.visualBell)return;var self=this;this.element.style.borderColor='white';setTimeout(function(){self.element.style.borderColor='';},10);if(this.popOnBell)this.focus();};/** + * Log the current state to the console. + */Terminal.prototype.log=function(){if(!this.debug)return;if(!this.context.console||!this.context.console.log)return;var args=Array.prototype.slice.call(arguments);this.context.console.log.apply(this.context.console,args);};/** + * Log the current state as error to the console. + */Terminal.prototype.error=function(){if(!this.debug)return;if(!this.context.console||!this.context.console.error)return;var args=Array.prototype.slice.call(arguments);this.context.console.error.apply(this.context.console,args);};/** + * Resizes the terminal. + * + * @param {number} x The number of columns to resize to. + * @param {number} y The number of rows to resize to. + */Terminal.prototype.resize=function(x,y){var line,el,i,j,ch,addToY;if(x===this.cols&&y===this.rows){return;}if(x<1)x=1;if(y<1)y=1;// resize cols +j=this.cols;if(j<x){ch=[this.defAttr,' ',1];// does xterm use the default attr? +i=this.lines.length;while(i--){while(this.lines[i].length<x){this.lines[i].push(ch);}}}else{// (j > x) +i=this.lines.length;while(i--){while(this.lines[i].length>x){this.lines[i].pop();}}}this.setupStops(j);this.cols=x;// resize rows +j=this.rows;addToY=0;if(j<y){el=this.element;while(j++<y){// y is rows, not this.y +if(this.lines.length<y+this.ybase){if(this.ybase>0&&this.lines.length<=this.ybase+this.y+addToY+1){// There is room above the buffer and there are no empty elements below the line, +// scroll up +this.ybase--;addToY++;if(this.ydisp>0){// Viewport is at the top of the buffer, must increase downwards +this.ydisp--;}}else{// Add a blank line if there is no buffer left at the top to scroll to, or if there +// are blank lines after the cursor +this.lines.push(this.blankLine());}}if(this.children.length<y){this.insertRow();}}}else{// (j > y) +while(j-->y){if(this.lines.length>y+this.ybase){if(this.lines.length>this.ybase+this.y+1){// The line is a blank line below the cursor, remove it +this.lines.pop();}else{// The line is the cursor, scroll down +this.ybase++;this.ydisp++;}}if(this.children.length>y){el=this.children.shift();if(!el)continue;el.parentNode.removeChild(el);}}}this.rows=y;// Make sure that the cursor stays on screen +if(this.y>=y){this.y=y-1;}if(addToY){this.y+=addToY;}if(this.x>=x){this.x=x-1;}this.scrollTop=0;this.scrollBottom=y-1;this.refresh(0,this.rows-1);this.normal=null;this.geometry=[this.cols,this.rows];this.emit('resize',{terminal:this,cols:x,rows:y});};/** + * Updates the range of rows to refresh + * @param {number} y The number of rows to refresh next. + */Terminal.prototype.updateRange=function(y){if(y<this.refreshStart)this.refreshStart=y;if(y>this.refreshEnd)this.refreshEnd=y;// if (y > this.refreshEnd) { +// this.refreshEnd = y; +// if (y > this.rows - 1) { +// this.refreshEnd = this.rows - 1; +// } +// } +};/** + * Set the range of refreshing to the maximum value + */Terminal.prototype.maxRange=function(){this.refreshStart=0;this.refreshEnd=this.rows-1;};/** + * Setup the tab stops. + * @param {number} i + */Terminal.prototype.setupStops=function(i){if(i!=null){if(!this.tabs[i]){i=this.prevStop(i);}}else{this.tabs={};i=0;}for(;i<this.cols;i+=8){this.tabs[i]=true;}};/** + * Move the cursor to the previous tab stop from the given position (default is current). + * @param {number} x The position to move the cursor to the previous tab stop. + */Terminal.prototype.prevStop=function(x){if(x==null)x=this.x;while(!this.tabs[--x]&&x>0){}return x>=this.cols?this.cols-1:x<0?0:x;};/** + * Move the cursor one tab stop forward from the given position (default is current). + * @param {number} x The position to move the cursor one tab stop forward. + */Terminal.prototype.nextStop=function(x){if(x==null)x=this.x;while(!this.tabs[++x]&&x<this.cols){}return x>=this.cols?this.cols-1:x<0?0:x;};/** + * Erase in the identified line everything from "x" to the end of the line (right). + * @param {number} x The column from which to start erasing to the end of the line. + * @param {number} y The line in which to operate. + */Terminal.prototype.eraseRight=function(x,y){var line=this.lines[this.ybase+y],ch=[this.eraseAttr(),' ',1];// xterm +for(;x<this.cols;x++){line[x]=ch;}this.updateRange(y);};/** + * Erase in the identified line everything from "x" to the start of the line (left). + * @param {number} x The column from which to start erasing to the start of the line. + * @param {number} y The line in which to operate. + */Terminal.prototype.eraseLeft=function(x,y){var line=this.lines[this.ybase+y],ch=[this.eraseAttr(),' ',1];// xterm +x++;while(x--){line[x]=ch;}this.updateRange(y);};/** + * Clears the entire buffer, making the prompt line the new first line. + */Terminal.prototype.clear=function(){if(this.ybase===0&&this.y===0){// Don't clear if it's already clear +return;}this.lines=[this.lines[this.ybase+this.y]];this.ydisp=0;this.ybase=0;this.y=0;for(var i=1;i<this.rows;i++){this.lines.push(this.blankLine());}this.refresh(0,this.rows-1);this.emit('scroll',this.ydisp);};/** + * Erase all content in the given line + * @param {number} y The line to erase all of its contents. + */Terminal.prototype.eraseLine=function(y){this.eraseRight(0,y);};/** + * Return the data array of a blank line/ + * @param {number} cur First bunch of data for each "blank" character. + */Terminal.prototype.blankLine=function(cur){var attr=cur?this.eraseAttr():this.defAttr;var ch=[attr,' ',1]// width defaults to 1 halfwidth character +,line=[],i=0;for(;i<this.cols;i++){line[i]=ch;}return line;};/** + * If cur return the back color xterm feature attribute. Else return defAttr. + * @param {object} cur + */Terminal.prototype.ch=function(cur){return cur?[this.eraseAttr(),' ',1]:[this.defAttr,' ',1];};/** + * Evaluate if the current erminal is the given argument. + * @param {object} term The terminal to evaluate + */Terminal.prototype.is=function(term){var name=this.termName;return(name+'').indexOf(term)===0;};/** + * Emit the 'data' event and populate the given data. + * @param {string} data The data to populate in the event. + */Terminal.prototype.handler=function(data){this.emit('data',data);};/** + * Emit the 'title' event and populate the given title. + * @param {string} title The title to populate in the event. + */Terminal.prototype.handleTitle=function(title){this.emit('title',title);};/** + * ESC + *//** + * ESC D Index (IND is 0x84). + */Terminal.prototype.index=function(){this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}this.state=normal;};/** + * ESC M Reverse Index (RI is 0x8d). + */Terminal.prototype.reverseIndex=function(){var j;this.y--;if(this.y<this.scrollTop){this.y++;// possibly move the code below to term.reverseScroll(); +// test: echo -ne '\e[1;1H\e[44m\eM\e[0m' +// blankLine(true) is xterm/linux behavior +this.lines.splice(this.y+this.ybase,0,this.blankLine(true));j=this.rows-1-this.scrollBottom;this.lines.splice(this.rows-1+this.ybase-j+1,1);// this.maxRange(); +this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);}this.state=normal;};/** + * ESC c Full Reset (RIS). + */Terminal.prototype.reset=function(){this.options.rows=this.rows;this.options.cols=this.cols;var customKeydownHandler=this.customKeydownHandler;Terminal.call(this,this.options);this.customKeydownHandler=customKeydownHandler;this.refresh(0,this.rows-1);this.viewport.syncScrollArea();};/** + * ESC H Tab Set (HTS is 0x88). + */Terminal.prototype.tabSet=function(){this.tabs[this.x]=true;this.state=normal;};/** + * CSI + *//** + * CSI Ps A + * Cursor Up Ps Times (default = 1) (CUU). + */Terminal.prototype.cursorUp=function(params){var param=params[0];if(param<1)param=1;this.y-=param;if(this.y<0)this.y=0;};/** + * CSI Ps B + * Cursor Down Ps Times (default = 1) (CUD). + */Terminal.prototype.cursorDown=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}};/** + * CSI Ps C + * Cursor Forward Ps Times (default = 1) (CUF). + */Terminal.prototype.cursorForward=function(params){var param=params[0];if(param<1)param=1;this.x+=param;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * CSI Ps D + * Cursor Backward Ps Times (default = 1) (CUB). + */Terminal.prototype.cursorBackward=function(params){var param=params[0];if(param<1)param=1;this.x-=param;if(this.x<0)this.x=0;};/** + * CSI Ps ; Ps H + * Cursor Position [row;column] (default = [1,1]) (CUP). + */Terminal.prototype.cursorPos=function(params){var row,col;row=params[0]-1;if(params.length>=2){col=params[1]-1;}else{col=0;}if(row<0){row=0;}else if(row>=this.rows){row=this.rows-1;}if(col<0){col=0;}else if(col>=this.cols){col=this.cols-1;}this.x=col;this.y=row;};/** + * CSI Ps J Erase in Display (ED). + * Ps = 0 -> Erase Below (default). + * Ps = 1 -> Erase Above. + * Ps = 2 -> Erase All. + * Ps = 3 -> Erase Saved Lines (xterm). + * CSI ? Ps J + * Erase in Display (DECSED). + * Ps = 0 -> Selective Erase Below (default). + * Ps = 1 -> Selective Erase Above. + * Ps = 2 -> Selective Erase All. + */Terminal.prototype.eraseInDisplay=function(params){var j;switch(params[0]){case 0:this.eraseRight(this.x,this.y);j=this.y+1;for(;j<this.rows;j++){this.eraseLine(j);}break;case 1:this.eraseLeft(this.x,this.y);j=this.y;while(j--){this.eraseLine(j);}break;case 2:j=this.rows;while(j--){this.eraseLine(j);}break;case 3:;// no saved lines +break;}};/** + * CSI Ps K Erase in Line (EL). + * Ps = 0 -> Erase to Right (default). + * Ps = 1 -> Erase to Left. + * Ps = 2 -> Erase All. + * CSI ? Ps K + * Erase in Line (DECSEL). + * Ps = 0 -> Selective Erase to Right (default). + * Ps = 1 -> Selective Erase to Left. + * Ps = 2 -> Selective Erase All. + */Terminal.prototype.eraseInLine=function(params){switch(params[0]){case 0:this.eraseRight(this.x,this.y);break;case 1:this.eraseLeft(this.x,this.y);break;case 2:this.eraseLine(this.y);break;}};/** + * CSI Pm m Character Attributes (SGR). + * Ps = 0 -> Normal (default). + * Ps = 1 -> Bold. + * Ps = 4 -> Underlined. + * Ps = 5 -> Blink (appears as Bold). + * Ps = 7 -> Inverse. + * Ps = 8 -> Invisible, i.e., hidden (VT300). + * Ps = 2 2 -> Normal (neither bold nor faint). + * Ps = 2 4 -> Not underlined. + * Ps = 2 5 -> Steady (not blinking). + * Ps = 2 7 -> Positive (not inverse). + * Ps = 2 8 -> Visible, i.e., not hidden (VT300). + * Ps = 3 0 -> Set foreground color to Black. + * Ps = 3 1 -> Set foreground color to Red. + * Ps = 3 2 -> Set foreground color to Green. + * Ps = 3 3 -> Set foreground color to Yellow. + * Ps = 3 4 -> Set foreground color to Blue. + * Ps = 3 5 -> Set foreground color to Magenta. + * Ps = 3 6 -> Set foreground color to Cyan. + * Ps = 3 7 -> Set foreground color to White. + * Ps = 3 9 -> Set foreground color to default (original). + * Ps = 4 0 -> Set background color to Black. + * Ps = 4 1 -> Set background color to Red. + * Ps = 4 2 -> Set background color to Green. + * Ps = 4 3 -> Set background color to Yellow. + * Ps = 4 4 -> Set background color to Blue. + * Ps = 4 5 -> Set background color to Magenta. + * Ps = 4 6 -> Set background color to Cyan. + * Ps = 4 7 -> Set background color to White. + * Ps = 4 9 -> Set background color to default (original). + * + * If 16-color support is compiled, the following apply. Assume + * that xterm's resources are set so that the ISO color codes are + * the first 8 of a set of 16. Then the aixterm colors are the + * bright versions of the ISO colors: + * Ps = 9 0 -> Set foreground color to Black. + * Ps = 9 1 -> Set foreground color to Red. + * Ps = 9 2 -> Set foreground color to Green. + * Ps = 9 3 -> Set foreground color to Yellow. + * Ps = 9 4 -> Set foreground color to Blue. + * Ps = 9 5 -> Set foreground color to Magenta. + * Ps = 9 6 -> Set foreground color to Cyan. + * Ps = 9 7 -> Set foreground color to White. + * Ps = 1 0 0 -> Set background color to Black. + * Ps = 1 0 1 -> Set background color to Red. + * Ps = 1 0 2 -> Set background color to Green. + * Ps = 1 0 3 -> Set background color to Yellow. + * Ps = 1 0 4 -> Set background color to Blue. + * Ps = 1 0 5 -> Set background color to Magenta. + * Ps = 1 0 6 -> Set background color to Cyan. + * Ps = 1 0 7 -> Set background color to White. + * + * If xterm is compiled with the 16-color support disabled, it + * supports the following, from rxvt: + * Ps = 1 0 0 -> Set foreground and background color to + * default. + * + * If 88- or 256-color support is compiled, the following apply. + * Ps = 3 8 ; 5 ; Ps -> Set foreground color to the second + * Ps. + * Ps = 4 8 ; 5 ; Ps -> Set background color to the second + * Ps. + */Terminal.prototype.charAttributes=function(params){// Optimize a single SGR0. +if(params.length===1&¶ms[0]===0){this.curAttr=this.defAttr;return;}var l=params.length,i=0,flags=this.curAttr>>18,fg=this.curAttr>>9&0x1ff,bg=this.curAttr&0x1ff,p;for(;i<l;i++){p=params[i];if(p>=30&&p<=37){// fg color 8 +fg=p-30;}else if(p>=40&&p<=47){// bg color 8 +bg=p-40;}else if(p>=90&&p<=97){// fg color 16 +p+=8;fg=p-90;}else if(p>=100&&p<=107){// bg color 16 +p+=8;bg=p-100;}else if(p===0){// default +flags=this.defAttr>>18;fg=this.defAttr>>9&0x1ff;bg=this.defAttr&0x1ff;// flags = 0; +// fg = 0x1ff; +// bg = 0x1ff; +}else if(p===1){// bold text +flags|=1;}else if(p===4){// underlined text +flags|=2;}else if(p===5){// blink +flags|=4;}else if(p===7){// inverse and positive +// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' +flags|=8;}else if(p===8){// invisible +flags|=16;}else if(p===22){// not bold +flags&=~1;}else if(p===24){// not underlined +flags&=~2;}else if(p===25){// not blink +flags&=~4;}else if(p===27){// not inverse +flags&=~8;}else if(p===28){// not invisible +flags&=~16;}else if(p===39){// reset fg +fg=this.defAttr>>9&0x1ff;}else if(p===49){// reset bg +bg=this.defAttr&0x1ff;}else if(p===38){// fg color 256 +if(params[i+1]===2){i+=2;fg=matchColor(params[i]&0xff,params[i+1]&0xff,params[i+2]&0xff);if(fg===-1)fg=0x1ff;i+=2;}else if(params[i+1]===5){i+=2;p=params[i]&0xff;fg=p;}}else if(p===48){// bg color 256 +if(params[i+1]===2){i+=2;bg=matchColor(params[i]&0xff,params[i+1]&0xff,params[i+2]&0xff);if(bg===-1)bg=0x1ff;i+=2;}else if(params[i+1]===5){i+=2;p=params[i]&0xff;bg=p;}}else if(p===100){// reset fg/bg +fg=this.defAttr>>9&0x1ff;bg=this.defAttr&0x1ff;}else{this.error('Unknown SGR attribute: %d.',p);}}this.curAttr=flags<<18|fg<<9|bg;};/** + * CSI Ps n Device Status Report (DSR). + * Ps = 5 -> Status Report. Result (``OK'') is + * CSI 0 n + * Ps = 6 -> Report Cursor Position (CPR) [row;column]. + * Result is + * CSI r ; c R + * CSI ? Ps n + * Device Status Report (DSR, DEC-specific). + * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI + * ? r ; c R (assumes page is zero). + * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). + * or CSI ? 1 1 n (not ready). + * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) + * or CSI ? 2 1 n (locked). + * Ps = 2 6 -> Report Keyboard status as + * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). + * The last two parameters apply to VT400 & up, and denote key- + * board ready and LK01 respectively. + * Ps = 5 3 -> Report Locator status as + * CSI ? 5 3 n Locator available, if compiled-in, or + * CSI ? 5 0 n No Locator, if not. + */Terminal.prototype.deviceStatus=function(params){if(!this.prefix){switch(params[0]){case 5:// status report +this.send('\x1b[0n');break;case 6:// cursor position +this.send('\x1b['+(this.y+1)+';'+(this.x+1)+'R');break;}}else if(this.prefix==='?'){// modern xterm doesnt seem to +// respond to any of these except ?6, 6, and 5 +switch(params[0]){case 6:// cursor position +this.send('\x1b[?'+(this.y+1)+';'+(this.x+1)+'R');break;case 15:// no printer +// this.send('\x1b[?11n'); +break;case 25:// dont support user defined keys +// this.send('\x1b[?21n'); +break;case 26:// north american keyboard +// this.send('\x1b[?27;1;0;0n'); +break;case 53:// no dec locator/mouse +// this.send('\x1b[?50n'); +break;}}};/** + * Additions + *//** + * CSI Ps @ + * Insert Ps (Blank) Character(s) (default = 1) (ICH). + */Terminal.prototype.insertChars=function(params){var param,row,j,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.x;ch=[this.eraseAttr(),' ',1];// xterm +while(param--&&j<this.cols){this.lines[row].splice(j++,0,ch);this.lines[row].pop();}};/** + * CSI Ps E + * Cursor Next Line Ps Times (default = 1) (CNL). + * same as CSI Ps B ? + */Terminal.prototype.cursorNextLine=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}this.x=0;};/** + * CSI Ps F + * Cursor Preceding Line Ps Times (default = 1) (CNL). + * reuse CSI Ps A ? + */Terminal.prototype.cursorPrecedingLine=function(params){var param=params[0];if(param<1)param=1;this.y-=param;if(this.y<0)this.y=0;this.x=0;};/** + * CSI Ps G + * Cursor Character Absolute [column] (default = [row,1]) (CHA). + */Terminal.prototype.cursorCharAbsolute=function(params){var param=params[0];if(param<1)param=1;this.x=param-1;};/** + * CSI Ps L + * Insert Ps Line(s) (default = 1) (IL). + */Terminal.prototype.insertLines=function(params){var param,row,j;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.rows-1-this.scrollBottom;j=this.rows-1+this.ybase-j+1;while(param--){// test: echo -e '\e[44m\e[1L\e[0m' +// blankLine(true) - xterm/linux behavior +this.lines.splice(row,0,this.blankLine(true));this.lines.splice(j,1);}// this.maxRange(); +this.updateRange(this.y);this.updateRange(this.scrollBottom);};/** + * CSI Ps M + * Delete Ps Line(s) (default = 1) (DL). + */Terminal.prototype.deleteLines=function(params){var param,row,j;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.rows-1-this.scrollBottom;j=this.rows-1+this.ybase-j;while(param--){// test: echo -e '\e[44m\e[1M\e[0m' +// blankLine(true) - xterm/linux behavior +this.lines.splice(j+1,0,this.blankLine(true));this.lines.splice(row,1);}// this.maxRange(); +this.updateRange(this.y);this.updateRange(this.scrollBottom);};/** + * CSI Ps P + * Delete Ps Character(s) (default = 1) (DCH). + */Terminal.prototype.deleteChars=function(params){var param,row,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;ch=[this.eraseAttr(),' ',1];// xterm +while(param--){this.lines[row].splice(this.x,1);this.lines[row].push(ch);}};/** + * CSI Ps X + * Erase Ps Character(s) (default = 1) (ECH). + */Terminal.prototype.eraseChars=function(params){var param,row,j,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.x;ch=[this.eraseAttr(),' ',1];// xterm +while(param--&&j<this.cols){this.lines[row][j++]=ch;}};/** + * CSI Pm ` Character Position Absolute + * [column] (default = [row,1]) (HPA). + */Terminal.prototype.charPosAbsolute=function(params){var param=params[0];if(param<1)param=1;this.x=param-1;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * 141 61 a * HPR - + * Horizontal Position Relative + * reuse CSI Ps C ? + */Terminal.prototype.HPositionRelative=function(params){var param=params[0];if(param<1)param=1;this.x+=param;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * CSI Ps c Send Device Attributes (Primary DA). + * Ps = 0 or omitted -> request attributes from terminal. The + * response depends on the decTerminalID resource setting. + * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'') + * -> CSI ? 1 ; 0 c (``VT101 with No Options'') + * -> CSI ? 6 c (``VT102'') + * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'') + * The VT100-style response parameters do not mean anything by + * themselves. VT220 parameters do, telling the host what fea- + * tures the terminal supports: + * Ps = 1 -> 132-columns. + * Ps = 2 -> Printer. + * Ps = 6 -> Selective erase. + * Ps = 8 -> User-defined keys. + * Ps = 9 -> National replacement character sets. + * Ps = 1 5 -> Technical characters. + * Ps = 2 2 -> ANSI color, e.g., VT525. + * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode). + * CSI > Ps c + * Send Device Attributes (Secondary DA). + * Ps = 0 or omitted -> request the terminal's identification + * code. The response depends on the decTerminalID resource set- + * ting. It should apply only to VT220 and up, but xterm extends + * this to VT100. + * -> CSI > Pp ; Pv ; Pc c + * where Pp denotes the terminal type + * Pp = 0 -> ``VT100''. + * Pp = 1 -> ``VT220''. + * and Pv is the firmware version (for xterm, this was originally + * the XFree86 patch number, starting with 95). In a DEC termi- + * nal, Pc indicates the ROM cartridge registration number and is + * always zero. + * More information: + * xterm/charproc.c - line 2012, for more information. + * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) + */Terminal.prototype.sendDeviceAttributes=function(params){if(params[0]>0)return;if(!this.prefix){if(this.is('xterm')||this.is('rxvt-unicode')||this.is('screen')){this.send('\x1b[?1;2c');}else if(this.is('linux')){this.send('\x1b[?6c');}}else if(this.prefix==='>'){// xterm and urxvt +// seem to spit this +// out around ~370 times (?). +if(this.is('xterm')){this.send('\x1b[>0;276;0c');}else if(this.is('rxvt-unicode')){this.send('\x1b[>85;95;0c');}else if(this.is('linux')){// not supported by linux console. +// linux console echoes parameters. +this.send(params[0]+'c');}else if(this.is('screen')){this.send('\x1b[>83;40003;0c');}}};/** + * CSI Pm d + * Line Position Absolute [row] (default = [1,column]) (VPA). + */Terminal.prototype.linePosAbsolute=function(params){var param=params[0];if(param<1)param=1;this.y=param-1;if(this.y>=this.rows){this.y=this.rows-1;}};/** + * 145 65 e * VPR - Vertical Position Relative + * reuse CSI Ps B ? + */Terminal.prototype.VPositionRelative=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}};/** + * CSI Ps ; Ps f + * Horizontal and Vertical Position [row;column] (default = + * [1,1]) (HVP). + */Terminal.prototype.HVPosition=function(params){if(params[0]<1)params[0]=1;if(params[1]<1)params[1]=1;this.y=params[0]-1;if(this.y>=this.rows){this.y=this.rows-1;}this.x=params[1]-1;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * CSI Pm h Set Mode (SM). + * Ps = 2 -> Keyboard Action Mode (AM). + * Ps = 4 -> Insert Mode (IRM). + * Ps = 1 2 -> Send/receive (SRM). + * Ps = 2 0 -> Automatic Newline (LNM). + * CSI ? Pm h + * DEC Private Mode Set (DECSET). + * Ps = 1 -> Application Cursor Keys (DECCKM). + * Ps = 2 -> Designate USASCII for character sets G0-G3 + * (DECANM), and set VT100 mode. + * Ps = 3 -> 132 Column Mode (DECCOLM). + * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM). + * Ps = 5 -> Reverse Video (DECSCNM). + * Ps = 6 -> Origin Mode (DECOM). + * Ps = 7 -> Wraparound Mode (DECAWM). + * Ps = 8 -> Auto-repeat Keys (DECARM). + * Ps = 9 -> Send Mouse X & Y on button press. See the sec- + * tion Mouse Tracking. + * Ps = 1 0 -> Show toolbar (rxvt). + * Ps = 1 2 -> Start Blinking Cursor (att610). + * Ps = 1 8 -> Print form feed (DECPFF). + * Ps = 1 9 -> Set print extent to full screen (DECPEX). + * Ps = 2 5 -> Show Cursor (DECTCEM). + * Ps = 3 0 -> Show scrollbar (rxvt). + * Ps = 3 5 -> Enable font-shifting functions (rxvt). + * Ps = 3 8 -> Enter Tektronix Mode (DECTEK). + * Ps = 4 0 -> Allow 80 -> 132 Mode. + * Ps = 4 1 -> more(1) fix (see curses resource). + * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN- + * RCM). + * Ps = 4 4 -> Turn On Margin Bell. + * Ps = 4 5 -> Reverse-wraparound Mode. + * Ps = 4 6 -> Start Logging. This is normally disabled by a + * compile-time option. + * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis- + * abled by the titeInhibit resource). + * Ps = 6 6 -> Application keypad (DECNKM). + * Ps = 6 7 -> Backarrow key sends backspace (DECBKM). + * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and + * release. See the section Mouse Tracking. + * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking. + * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking. + * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking. + * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events. + * Ps = 1 0 0 5 -> Enable Extended Mouse Mode. + * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt). + * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt). + * Ps = 1 0 3 4 -> Interpret "meta" key, sets eighth bit. + * (enables the eightBitInput resource). + * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num- + * Lock keys. (This enables the numLock resource). + * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This + * enables the metaSendsEscape resource). + * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete + * key. + * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This + * enables the altSendsEscape resource). + * Ps = 1 0 4 0 -> Keep selection even if not highlighted. + * (This enables the keepSelection resource). + * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables + * the selectToClipboard resource). + * Ps = 1 0 4 2 -> Enable Urgency window manager hint when + * Control-G is received. (This enables the bellIsUrgent + * resource). + * Ps = 1 0 4 3 -> Enable raising of the window when Control-G + * is received. (enables the popOnBell resource). + * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be + * disabled by the titeInhibit resource). + * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis- + * abled by the titeInhibit resource). + * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate + * Screen Buffer, clearing it first. (This may be disabled by + * the titeInhibit resource). This combines the effects of the 1 + * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based + * applications rather than the 4 7 mode. + * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode. + * Ps = 1 0 5 1 -> Set Sun function-key mode. + * Ps = 1 0 5 2 -> Set HP function-key mode. + * Ps = 1 0 5 3 -> Set SCO function-key mode. + * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6). + * Ps = 1 0 6 1 -> Set VT220 keyboard emulation. + * Ps = 2 0 0 4 -> Set bracketed paste mode. + * Modes: + * http: *vt100.net/docs/vt220-rm/chapter4.html + */Terminal.prototype.setMode=function(params){if((typeof params==='undefined'?'undefined':_typeof(params))==='object'){var l=params.length,i=0;for(;i<l;i++){this.setMode(params[i]);}return;}if(!this.prefix){switch(params){case 4:this.insertMode=true;break;case 20://this.convertEol = true; +break;}}else if(this.prefix==='?'){switch(params){case 1:this.applicationCursor=true;break;case 2:this.setgCharset(0,Terminal.charsets.US);this.setgCharset(1,Terminal.charsets.US);this.setgCharset(2,Terminal.charsets.US);this.setgCharset(3,Terminal.charsets.US);// set VT100 mode here +break;case 3:// 132 col mode +this.savedCols=this.cols;this.resize(132,this.rows);break;case 6:this.originMode=true;break;case 7:this.wraparoundMode=true;break;case 12:// this.cursorBlink = true; +break;case 66:this.log('Serial port requested application keypad.');this.applicationKeypad=true;this.viewport.syncScrollArea();break;case 9:// X10 Mouse +// no release, no motion, no wheel, no modifiers. +case 1000:// vt200 mouse +// no motion. +// no modifiers, except control on the wheel. +case 1002:// button event mouse +case 1003:// any event mouse +// any event - sends motion events, +// even if there is no button held down. +this.x10Mouse=params===9;this.vt200Mouse=params===1000;this.normalMouse=params>1000;this.mouseEvents=true;this.element.style.cursor='default';this.log('Binding to mouse events.');break;case 1004:// send focusin/focusout events +// focusin: ^[[I +// focusout: ^[[O +this.sendFocus=true;break;case 1005:// utf8 ext mode mouse +this.utfMouse=true;// for wide terminals +// simply encodes large values as utf8 characters +break;case 1006:// sgr ext mode mouse +this.sgrMouse=true;// for wide terminals +// does not add 32 to fields +// press: ^[[<b;x;yM +// release: ^[[<b;x;ym +break;case 1015:// urxvt ext mode mouse +this.urxvtMouse=true;// for wide terminals +// numbers for fields +// press: ^[[b;x;yM +// motion: ^[[b;x;yT +break;case 25:// show cursor +this.cursorHidden=false;break;case 1049:// alt screen buffer cursor +//this.saveCursor(); +;// FALL-THROUGH +case 47:// alt screen buffer +case 1047:// alt screen buffer +if(!this.normal){var normal={lines:this.lines,ybase:this.ybase,ydisp:this.ydisp,x:this.x,y:this.y,scrollTop:this.scrollTop,scrollBottom:this.scrollBottom,tabs:this.tabs// XXX save charset(s) here? +// charset: this.charset, +// glevel: this.glevel, +// charsets: this.charsets +};this.reset();this.normal=normal;this.showCursor();}break;}}};/** + * CSI Pm l Reset Mode (RM). + * Ps = 2 -> Keyboard Action Mode (AM). + * Ps = 4 -> Replace Mode (IRM). + * Ps = 1 2 -> Send/receive (SRM). + * Ps = 2 0 -> Normal Linefeed (LNM). + * CSI ? Pm l + * DEC Private Mode Reset (DECRST). + * Ps = 1 -> Normal Cursor Keys (DECCKM). + * Ps = 2 -> Designate VT52 mode (DECANM). + * Ps = 3 -> 80 Column Mode (DECCOLM). + * Ps = 4 -> Jump (Fast) Scroll (DECSCLM). + * Ps = 5 -> Normal Video (DECSCNM). + * Ps = 6 -> Normal Cursor Mode (DECOM). + * Ps = 7 -> No Wraparound Mode (DECAWM). + * Ps = 8 -> No Auto-repeat Keys (DECARM). + * Ps = 9 -> Don't send Mouse X & Y on button press. + * Ps = 1 0 -> Hide toolbar (rxvt). + * Ps = 1 2 -> Stop Blinking Cursor (att610). + * Ps = 1 8 -> Don't print form feed (DECPFF). + * Ps = 1 9 -> Limit print to scrolling region (DECPEX). + * Ps = 2 5 -> Hide Cursor (DECTCEM). + * Ps = 3 0 -> Don't show scrollbar (rxvt). + * Ps = 3 5 -> Disable font-shifting functions (rxvt). + * Ps = 4 0 -> Disallow 80 -> 132 Mode. + * Ps = 4 1 -> No more(1) fix (see curses resource). + * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC- + * NRCM). + * Ps = 4 4 -> Turn Off Margin Bell. + * Ps = 4 5 -> No Reverse-wraparound Mode. + * Ps = 4 6 -> Stop Logging. (This is normally disabled by a + * compile-time option). + * Ps = 4 7 -> Use Normal Screen Buffer. + * Ps = 6 6 -> Numeric keypad (DECNKM). + * Ps = 6 7 -> Backarrow key sends delete (DECBKM). + * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and + * release. See the section Mouse Tracking. + * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking. + * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking. + * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking. + * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events. + * Ps = 1 0 0 5 -> Disable Extended Mouse Mode. + * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output + * (rxvt). + * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt). + * Ps = 1 0 3 4 -> Don't interpret "meta" key. (This disables + * the eightBitInput resource). + * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num- + * Lock keys. (This disables the numLock resource). + * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key. + * (This disables the metaSendsEscape resource). + * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad + * Delete key. + * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key. + * (This disables the altSendsEscape resource). + * Ps = 1 0 4 0 -> Do not keep selection when not highlighted. + * (This disables the keepSelection resource). + * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables + * the selectToClipboard resource). + * Ps = 1 0 4 2 -> Disable Urgency window manager hint when + * Control-G is received. (This disables the bellIsUrgent + * resource). + * Ps = 1 0 4 3 -> Disable raising of the window when Control- + * G is received. (This disables the popOnBell resource). + * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen + * first if in the Alternate Screen. (This may be disabled by + * the titeInhibit resource). + * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be + * disabled by the titeInhibit resource). + * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor + * as in DECRC. (This may be disabled by the titeInhibit + * resource). This combines the effects of the 1 0 4 7 and 1 0 + * 4 8 modes. Use this with terminfo-based applications rather + * than the 4 7 mode. + * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode. + * Ps = 1 0 5 1 -> Reset Sun function-key mode. + * Ps = 1 0 5 2 -> Reset HP function-key mode. + * Ps = 1 0 5 3 -> Reset SCO function-key mode. + * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6). + * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. + * Ps = 2 0 0 4 -> Reset bracketed paste mode. + */Terminal.prototype.resetMode=function(params){if((typeof params==='undefined'?'undefined':_typeof(params))==='object'){var l=params.length,i=0;for(;i<l;i++){this.resetMode(params[i]);}return;}if(!this.prefix){switch(params){case 4:this.insertMode=false;break;case 20://this.convertEol = false; +break;}}else if(this.prefix==='?'){switch(params){case 1:this.applicationCursor=false;break;case 3:if(this.cols===132&&this.savedCols){this.resize(this.savedCols,this.rows);}delete this.savedCols;break;case 6:this.originMode=false;break;case 7:this.wraparoundMode=false;break;case 12:// this.cursorBlink = false; +break;case 66:this.log('Switching back to normal keypad.');this.applicationKeypad=false;this.viewport.syncScrollArea();break;case 9:// X10 Mouse +case 1000:// vt200 mouse +case 1002:// button event mouse +case 1003:// any event mouse +this.x10Mouse=false;this.vt200Mouse=false;this.normalMouse=false;this.mouseEvents=false;this.element.style.cursor='';break;case 1004:// send focusin/focusout events +this.sendFocus=false;break;case 1005:// utf8 ext mode mouse +this.utfMouse=false;break;case 1006:// sgr ext mode mouse +this.sgrMouse=false;break;case 1015:// urxvt ext mode mouse +this.urxvtMouse=false;break;case 25:// hide cursor +this.cursorHidden=true;break;case 1049:// alt screen buffer cursor +;// FALL-THROUGH +case 47:// normal screen buffer +case 1047:// normal screen buffer - clearing it first +if(this.normal){this.lines=this.normal.lines;this.ybase=this.normal.ybase;this.ydisp=this.normal.ydisp;this.x=this.normal.x;this.y=this.normal.y;this.scrollTop=this.normal.scrollTop;this.scrollBottom=this.normal.scrollBottom;this.tabs=this.normal.tabs;this.normal=null;// if (params === 1049) { +// this.x = this.savedX; +// this.y = this.savedY; +// } +this.refresh(0,this.rows-1);this.showCursor();}break;}}};/** + * CSI Ps ; Ps r + * Set Scrolling Region [top;bottom] (default = full size of win- + * dow) (DECSTBM). + * CSI ? Pm r + */Terminal.prototype.setScrollRegion=function(params){if(this.prefix)return;this.scrollTop=(params[0]||1)-1;this.scrollBottom=(params[1]||this.rows)-1;this.x=0;this.y=0;};/** + * CSI s + * Save cursor (ANSI.SYS). + */Terminal.prototype.saveCursor=function(params){this.savedX=this.x;this.savedY=this.y;};/** + * CSI u + * Restore cursor (ANSI.SYS). + */Terminal.prototype.restoreCursor=function(params){this.x=this.savedX||0;this.y=this.savedY||0;};/** + * Lesser Used + *//** + * CSI Ps I + * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + */Terminal.prototype.cursorForwardTab=function(params){var param=params[0]||1;while(param--){this.x=this.nextStop();}};/** + * CSI Ps S Scroll up Ps lines (default = 1) (SU). + */Terminal.prototype.scrollUp=function(params){var param=params[0]||1;while(param--){this.lines.splice(this.ybase+this.scrollTop,1);this.lines.splice(this.ybase+this.scrollBottom,0,this.blankLine());}// this.maxRange(); +this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);};/** + * CSI Ps T Scroll down Ps lines (default = 1) (SD). + */Terminal.prototype.scrollDown=function(params){var param=params[0]||1;while(param--){this.lines.splice(this.ybase+this.scrollBottom,1);this.lines.splice(this.ybase+this.scrollTop,0,this.blankLine());}// this.maxRange(); +this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);};/** + * CSI Ps ; Ps ; Ps ; Ps ; Ps T + * Initiate highlight mouse tracking. Parameters are + * [func;startx;starty;firstrow;lastrow]. See the section Mouse + * Tracking. + */Terminal.prototype.initMouseTracking=function(params){// Relevant: DECSET 1001 +};/** + * CSI > Ps; Ps T + * Reset one or more features of the title modes to the default + * value. Normally, "reset" disables the feature. It is possi- + * ble to disable the ability to reset features by compiling a + * different default for the title modes into xterm. + * Ps = 0 -> Do not set window/icon labels using hexadecimal. + * Ps = 1 -> Do not query window/icon labels using hexadeci- + * mal. + * Ps = 2 -> Do not set window/icon labels using UTF-8. + * Ps = 3 -> Do not query window/icon labels using UTF-8. + * (See discussion of "Title Modes"). + */Terminal.prototype.resetTitleModes=function(params){;};/** + * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + */Terminal.prototype.cursorBackwardTab=function(params){var param=params[0]||1;while(param--){this.x=this.prevStop();}};/** + * CSI Ps b Repeat the preceding graphic character Ps times (REP). + */Terminal.prototype.repeatPrecedingCharacter=function(params){var param=params[0]||1,line=this.lines[this.ybase+this.y],ch=line[this.x-1]||[this.defAttr,' ',1];while(param--){line[this.x++]=ch;}};/** + * CSI Ps g Tab Clear (TBC). + * Ps = 0 -> Clear Current Column (default). + * Ps = 3 -> Clear All. + * Potentially: + * Ps = 2 -> Clear Stops on Line. + * http://vt100.net/annarbor/aaa-ug/section6.html + */Terminal.prototype.tabClear=function(params){var param=params[0];if(param<=0){delete this.tabs[this.x];}else if(param===3){this.tabs={};}};/** + * CSI Pm i Media Copy (MC). + * Ps = 0 -> Print screen (default). + * Ps = 4 -> Turn off printer controller mode. + * Ps = 5 -> Turn on printer controller mode. + * CSI ? Pm i + * Media Copy (MC, DEC-specific). + * Ps = 1 -> Print line containing cursor. + * Ps = 4 -> Turn off autoprint mode. + * Ps = 5 -> Turn on autoprint mode. + * Ps = 1 0 -> Print composed display, ignores DECPEX. + * Ps = 1 1 -> Print all pages. + */Terminal.prototype.mediaCopy=function(params){;};/** + * CSI > Ps; Ps m + * Set or reset resource-values used by xterm to decide whether + * to construct escape sequences holding information about the + * modifiers pressed with a given key. The first parameter iden- + * tifies the resource to set/reset. The second parameter is the + * value to assign to the resource. If the second parameter is + * omitted, the resource is reset to its initial value. + * Ps = 1 -> modifyCursorKeys. + * Ps = 2 -> modifyFunctionKeys. + * Ps = 4 -> modifyOtherKeys. + * If no parameters are given, all resources are reset to their + * initial values. + */Terminal.prototype.setResources=function(params){;};/** + * CSI > Ps n + * Disable modifiers which may be enabled via the CSI > Ps; Ps m + * sequence. This corresponds to a resource value of "-1", which + * cannot be set with the other sequence. The parameter identi- + * fies the resource to be disabled: + * Ps = 1 -> modifyCursorKeys. + * Ps = 2 -> modifyFunctionKeys. + * Ps = 4 -> modifyOtherKeys. + * If the parameter is omitted, modifyFunctionKeys is disabled. + * When modifyFunctionKeys is disabled, xterm uses the modifier + * keys to make an extended sequence of functions rather than + * adding a parameter to each function key to denote the modi- + * fiers. + */Terminal.prototype.disableModifiers=function(params){;};/** + * CSI > Ps p + * Set resource value pointerMode. This is used by xterm to + * decide whether to hide the pointer cursor as the user types. + * Valid values for the parameter: + * Ps = 0 -> never hide the pointer. + * Ps = 1 -> hide if the mouse tracking mode is not enabled. + * Ps = 2 -> always hide the pointer. If no parameter is + * given, xterm uses the default, which is 1 . + */Terminal.prototype.setPointerMode=function(params){;};/** + * CSI ! p Soft terminal reset (DECSTR). + * http://vt100.net/docs/vt220-rm/table4-10.html + */Terminal.prototype.softReset=function(params){this.cursorHidden=false;this.insertMode=false;this.originMode=false;this.wraparoundMode=false;// autowrap +this.applicationKeypad=false;// ? +this.viewport.syncScrollArea();this.applicationCursor=false;this.scrollTop=0;this.scrollBottom=this.rows-1;this.curAttr=this.defAttr;this.x=this.y=0;// ? +this.charset=null;this.glevel=0;// ?? +this.charsets=[null];// ?? +};/** + * CSI Ps$ p + * Request ANSI mode (DECRQM). For VT300 and up, reply is + * CSI Ps; Pm$ y + * where Ps is the mode number as in RM, and Pm is the mode + * value: + * 0 - not recognized + * 1 - set + * 2 - reset + * 3 - permanently set + * 4 - permanently reset + */Terminal.prototype.requestAnsiMode=function(params){;};/** + * CSI ? Ps$ p + * Request DEC private mode (DECRQM). For VT300 and up, reply is + * CSI ? Ps; Pm$ p + * where Ps is the mode number as in DECSET, Pm is the mode value + * as in the ANSI DECRQM. + */Terminal.prototype.requestPrivateMode=function(params){;};/** + * CSI Ps ; Ps " p + * Set conformance level (DECSCL). Valid values for the first + * parameter: + * Ps = 6 1 -> VT100. + * Ps = 6 2 -> VT200. + * Ps = 6 3 -> VT300. + * Valid values for the second parameter: + * Ps = 0 -> 8-bit controls. + * Ps = 1 -> 7-bit controls (always set for VT100). + * Ps = 2 -> 8-bit controls. + */Terminal.prototype.setConformanceLevel=function(params){;};/** + * CSI Ps q Load LEDs (DECLL). + * Ps = 0 -> Clear all LEDS (default). + * Ps = 1 -> Light Num Lock. + * Ps = 2 -> Light Caps Lock. + * Ps = 3 -> Light Scroll Lock. + * Ps = 2 1 -> Extinguish Num Lock. + * Ps = 2 2 -> Extinguish Caps Lock. + * Ps = 2 3 -> Extinguish Scroll Lock. + */Terminal.prototype.loadLEDs=function(params){;};/** + * CSI Ps SP q + * Set cursor style (DECSCUSR, VT520). + * Ps = 0 -> blinking block. + * Ps = 1 -> blinking block (default). + * Ps = 2 -> steady block. + * Ps = 3 -> blinking underline. + * Ps = 4 -> steady underline. + */Terminal.prototype.setCursorStyle=function(params){;};/** + * CSI Ps " q + * Select character protection attribute (DECSCA). Valid values + * for the parameter: + * Ps = 0 -> DECSED and DECSEL can erase (default). + * Ps = 1 -> DECSED and DECSEL cannot erase. + * Ps = 2 -> DECSED and DECSEL can erase. + */Terminal.prototype.setCharProtectionAttr=function(params){;};/** + * CSI ? Pm r + * Restore DEC Private Mode Values. The value of Ps previously + * saved is restored. Ps values are the same as for DECSET. + */Terminal.prototype.restorePrivateValues=function(params){;};/** + * CSI Pt; Pl; Pb; Pr; Ps$ r + * Change Attributes in Rectangular Area (DECCARA), VT400 and up. + * Pt; Pl; Pb; Pr denotes the rectangle. + * Ps denotes the SGR attributes to change: 0, 1, 4, 5, 7. + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.setAttrInRectangle=function(params){var t=params[0],l=params[1],b=params[2],r=params[3],attr=params[4];var line,i;for(;t<b+1;t++){line=this.lines[this.ybase+t];for(i=l;i<r;i++){line[i]=[attr,line[i][1]];}}// this.maxRange(); +this.updateRange(params[0]);this.updateRange(params[2]);};/** + * CSI Pc; Pt; Pl; Pb; Pr$ x + * Fill Rectangular Area (DECFRA), VT420 and up. + * Pc is the character to use. + * Pt; Pl; Pb; Pr denotes the rectangle. + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.fillRectangle=function(params){var ch=params[0],t=params[1],l=params[2],b=params[3],r=params[4];var line,i;for(;t<b+1;t++){line=this.lines[this.ybase+t];for(i=l;i<r;i++){line[i]=[line[i][0],String.fromCharCode(ch)];}}// this.maxRange(); +this.updateRange(params[1]);this.updateRange(params[3]);};/** + * CSI Ps ; Pu ' z + * Enable Locator Reporting (DECELR). + * Valid values for the first parameter: + * Ps = 0 -> Locator disabled (default). + * Ps = 1 -> Locator enabled. + * Ps = 2 -> Locator enabled for one report, then disabled. + * The second parameter specifies the coordinate unit for locator + * reports. + * Valid values for the second parameter: + * Pu = 0 <- or omitted -> default to character cells. + * Pu = 1 <- device physical pixels. + * Pu = 2 <- character cells. + */Terminal.prototype.enableLocatorReporting=function(params){var val=params[0]>0;//this.mouseEvents = val; +//this.decLocator = val; +};/** + * CSI Pt; Pl; Pb; Pr$ z + * Erase Rectangular Area (DECERA), VT400 and up. + * Pt; Pl; Pb; Pr denotes the rectangle. + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.eraseRectangle=function(params){var t=params[0],l=params[1],b=params[2],r=params[3];var line,i,ch;ch=[this.eraseAttr(),' ',1];// xterm? +for(;t<b+1;t++){line=this.lines[this.ybase+t];for(i=l;i<r;i++){line[i]=ch;}}// this.maxRange(); +this.updateRange(params[0]);this.updateRange(params[2]);};/** + * CSI P m SP } + * Insert P s Column(s) (default = 1) (DECIC), VT420 and up. + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.insertColumns=function(){var param=params[0],l=this.ybase+this.rows,ch=[this.eraseAttr(),' ',1]// xterm? +,i;while(param--){for(i=this.ybase;i<l;i++){this.lines[i].splice(this.x+1,0,ch);this.lines[i].pop();}}this.maxRange();};/** + * CSI P m SP ~ + * Delete P s Column(s) (default = 1) (DECDC), VT420 and up + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.deleteColumns=function(){var param=params[0],l=this.ybase+this.rows,ch=[this.eraseAttr(),' ',1]// xterm? +,i;while(param--){for(i=this.ybase;i<l;i++){this.lines[i].splice(this.x,1);this.lines[i].push(ch);}}this.maxRange();};/** + * Character Sets + */Terminal.charsets={};// DEC Special Character and Line Drawing Set. +// http://vt100.net/docs/vt102-ug/table5-13.html +// A lot of curses apps use this if they see TERM=xterm. +// testing: echo -e '\e(0a\e(B' +// The xterm output sometimes seems to conflict with the +// reference above. xterm seems in line with the reference +// when running vttest however. +// The table below now uses xterm's output from vttest. +Terminal.charsets.SCLD={// (0 +'`':'\u25C6',// 'â—†' +'a':'\u2592',// 'â–’' +'b':'\t',// '\t' +'c':'\f',// '\f' +'d':'\r',// '\r' +'e':'\n',// '\n' +'f':'\xB0',// '°' +'g':'\xB1',// '±' +'h':'\u2424',// '\u2424' (NL) +'i':'\x0B',// '\v' +'j':'\u2518',// '┘' +'k':'\u2510',// 'â”' +'l':'\u250C',// '┌' +'m':'\u2514',// 'â””' +'n':'\u253C',// '┼' +'o':'\u23BA',// '⎺' +'p':'\u23BB',// '⎻' +'q':'\u2500',// '─' +'r':'\u23BC',// '⎼' +'s':'\u23BD',// '⎽' +'t':'\u251C',// '├' +'u':'\u2524',// '┤' +'v':'\u2534',// 'â”´' +'w':'\u252C',// '┬' +'x':'\u2502',// '│' +'y':'\u2264',// '≤' +'z':'\u2265',// '≥' +'{':'\u03C0',// 'Ï€' +'|':'\u2260',// '≠' +'}':'\xA3',// '£' +'~':'\xB7'// '·' +};Terminal.charsets.UK=null;// (A +Terminal.charsets.US=null;// (B (USASCII) +Terminal.charsets.Dutch=null;// (4 +Terminal.charsets.Finnish=null;// (C or (5 +Terminal.charsets.French=null;// (R +Terminal.charsets.FrenchCanadian=null;// (Q +Terminal.charsets.German=null;// (K +Terminal.charsets.Italian=null;// (Y +Terminal.charsets.NorwegianDanish=null;// (E or (6 +Terminal.charsets.Spanish=null;// (Z +Terminal.charsets.Swedish=null;// (H or (7 +Terminal.charsets.Swiss=null;// (= +Terminal.charsets.ISOLatin=null;// /A +/** + * Helpers + */function on(el,type,handler,capture){if(!Array.isArray(el)){el=[el];}el.forEach(function(element){element.addEventListener(type,handler,capture||false);});}function off(el,type,handler,capture){el.removeEventListener(type,handler,capture||false);}function cancel(ev,force){if(!this.cancelEvents&&!force){return;}ev.preventDefault();ev.stopPropagation();return false;}function inherits(child,parent){function f(){this.constructor=child;}f.prototype=parent.prototype;child.prototype=new f();}// if bold is broken, we can't +// use it in the terminal. +function isBoldBroken(document){var body=document.getElementsByTagName('body')[0];var el=document.createElement('span');el.innerHTML='hello world';body.appendChild(el);var w1=el.scrollWidth;el.style.fontWeight='bold';var w2=el.scrollWidth;body.removeChild(el);return w1!==w2;}function indexOf(obj,el){var i=obj.length;while(i--){if(obj[i]===el)return i;}return-1;}function isThirdLevelShift(term,ev){var thirdLevelKey=term.browser.isMac&&ev.altKey&&!ev.ctrlKey&&!ev.metaKey||term.browser.isMSWindows&&ev.altKey&&ev.ctrlKey&&!ev.metaKey;if(ev.type=='keypress'){return thirdLevelKey;}// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events) +return thirdLevelKey&&(!ev.keyCode||ev.keyCode>47);}function matchColor(r1,g1,b1){var hash=r1<<16|g1<<8|b1;if(matchColor._cache[hash]!=null){return matchColor._cache[hash];}var ldiff=Infinity,li=-1,i=0,c,r2,g2,b2,diff;for(;i<Terminal.vcolors.length;i++){c=Terminal.vcolors[i];r2=c[0];g2=c[1];b2=c[2];diff=matchColor.distance(r1,g1,b1,r2,g2,b2);if(diff===0){li=i;break;}if(diff<ldiff){ldiff=diff;li=i;}}return matchColor._cache[hash]=li;}matchColor._cache={};// http://stackoverflow.com/questions/1633828 +matchColor.distance=function(r1,g1,b1,r2,g2,b2){return Math.pow(30*(r1-r2),2)+Math.pow(59*(g1-g2),2)+Math.pow(11*(b1-b2),2);};function each(obj,iter,con){if(obj.forEach)return obj.forEach(iter,con);for(var i=0;i<obj.length;i++){iter.call(con,obj[i],i,obj);}}function keys(obj){if(Object.keys)return Object.keys(obj);var key,keys=[];for(key in obj){if(Object.prototype.hasOwnProperty.call(obj,key)){keys.push(key);}}return keys;}var wcwidth=function(opts){// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c +// combining characters +var COMBINING=[[0x0300,0x036F],[0x0483,0x0486],[0x0488,0x0489],[0x0591,0x05BD],[0x05BF,0x05BF],[0x05C1,0x05C2],[0x05C4,0x05C5],[0x05C7,0x05C7],[0x0600,0x0603],[0x0610,0x0615],[0x064B,0x065E],[0x0670,0x0670],[0x06D6,0x06E4],[0x06E7,0x06E8],[0x06EA,0x06ED],[0x070F,0x070F],[0x0711,0x0711],[0x0730,0x074A],[0x07A6,0x07B0],[0x07EB,0x07F3],[0x0901,0x0902],[0x093C,0x093C],[0x0941,0x0948],[0x094D,0x094D],[0x0951,0x0954],[0x0962,0x0963],[0x0981,0x0981],[0x09BC,0x09BC],[0x09C1,0x09C4],[0x09CD,0x09CD],[0x09E2,0x09E3],[0x0A01,0x0A02],[0x0A3C,0x0A3C],[0x0A41,0x0A42],[0x0A47,0x0A48],[0x0A4B,0x0A4D],[0x0A70,0x0A71],[0x0A81,0x0A82],[0x0ABC,0x0ABC],[0x0AC1,0x0AC5],[0x0AC7,0x0AC8],[0x0ACD,0x0ACD],[0x0AE2,0x0AE3],[0x0B01,0x0B01],[0x0B3C,0x0B3C],[0x0B3F,0x0B3F],[0x0B41,0x0B43],[0x0B4D,0x0B4D],[0x0B56,0x0B56],[0x0B82,0x0B82],[0x0BC0,0x0BC0],[0x0BCD,0x0BCD],[0x0C3E,0x0C40],[0x0C46,0x0C48],[0x0C4A,0x0C4D],[0x0C55,0x0C56],[0x0CBC,0x0CBC],[0x0CBF,0x0CBF],[0x0CC6,0x0CC6],[0x0CCC,0x0CCD],[0x0CE2,0x0CE3],[0x0D41,0x0D43],[0x0D4D,0x0D4D],[0x0DCA,0x0DCA],[0x0DD2,0x0DD4],[0x0DD6,0x0DD6],[0x0E31,0x0E31],[0x0E34,0x0E3A],[0x0E47,0x0E4E],[0x0EB1,0x0EB1],[0x0EB4,0x0EB9],[0x0EBB,0x0EBC],[0x0EC8,0x0ECD],[0x0F18,0x0F19],[0x0F35,0x0F35],[0x0F37,0x0F37],[0x0F39,0x0F39],[0x0F71,0x0F7E],[0x0F80,0x0F84],[0x0F86,0x0F87],[0x0F90,0x0F97],[0x0F99,0x0FBC],[0x0FC6,0x0FC6],[0x102D,0x1030],[0x1032,0x1032],[0x1036,0x1037],[0x1039,0x1039],[0x1058,0x1059],[0x1160,0x11FF],[0x135F,0x135F],[0x1712,0x1714],[0x1732,0x1734],[0x1752,0x1753],[0x1772,0x1773],[0x17B4,0x17B5],[0x17B7,0x17BD],[0x17C6,0x17C6],[0x17C9,0x17D3],[0x17DD,0x17DD],[0x180B,0x180D],[0x18A9,0x18A9],[0x1920,0x1922],[0x1927,0x1928],[0x1932,0x1932],[0x1939,0x193B],[0x1A17,0x1A18],[0x1B00,0x1B03],[0x1B34,0x1B34],[0x1B36,0x1B3A],[0x1B3C,0x1B3C],[0x1B42,0x1B42],[0x1B6B,0x1B73],[0x1DC0,0x1DCA],[0x1DFE,0x1DFF],[0x200B,0x200F],[0x202A,0x202E],[0x2060,0x2063],[0x206A,0x206F],[0x20D0,0x20EF],[0x302A,0x302F],[0x3099,0x309A],[0xA806,0xA806],[0xA80B,0xA80B],[0xA825,0xA826],[0xFB1E,0xFB1E],[0xFE00,0xFE0F],[0xFE20,0xFE23],[0xFEFF,0xFEFF],[0xFFF9,0xFFFB],[0x10A01,0x10A03],[0x10A05,0x10A06],[0x10A0C,0x10A0F],[0x10A38,0x10A3A],[0x10A3F,0x10A3F],[0x1D167,0x1D169],[0x1D173,0x1D182],[0x1D185,0x1D18B],[0x1D1AA,0x1D1AD],[0x1D242,0x1D244],[0xE0001,0xE0001],[0xE0020,0xE007F],[0xE0100,0xE01EF]];// binary search +function bisearch(ucs){var min=0;var max=COMBINING.length-1;var mid;if(ucs<COMBINING[0][0]||ucs>COMBINING[max][1])return false;while(max>=min){mid=Math.floor((min+max)/2);if(ucs>COMBINING[mid][1])min=mid+1;else if(ucs<COMBINING[mid][0])max=mid-1;else return true;}return false;}function wcwidth(ucs){// test for 8-bit control characters +if(ucs===0)return opts.nul;if(ucs<32||ucs>=0x7f&&ucs<0xa0)return opts.control;// binary search in table of non-spacing characters +if(bisearch(ucs))return 0;// if we arrive here, ucs is not a combining or C0/C1 control character +return 1+(ucs>=0x1100&&(ucs<=0x115f||// Hangul Jamo init. consonants +ucs==0x2329||ucs==0x232a||ucs>=0x2e80&&ucs<=0xa4cf&&ucs!=0x303f||// CJK..Yi +ucs>=0xac00&&ucs<=0xd7a3||// Hangul Syllables +ucs>=0xf900&&ucs<=0xfaff||// CJK Compat Ideographs +ucs>=0xfe10&&ucs<=0xfe19||// Vertical forms +ucs>=0xfe30&&ucs<=0xfe6f||// CJK Compat Forms +ucs>=0xff00&&ucs<=0xff60||// Fullwidth Forms +ucs>=0xffe0&&ucs<=0xffe6||ucs>=0x20000&&ucs<=0x2fffd||ucs>=0x30000&&ucs<=0x3fffd));}return wcwidth;}({nul:0,control:0});// configurable options +/** + * Expose + */Terminal.EventEmitter=_EventEmitter.EventEmitter;Terminal.CompositionHelper=_CompositionHelper.CompositionHelper;Terminal.Viewport=_Viewport.Viewport;Terminal.inherits=inherits;/** + * Adds an event listener to the terminal. + * + * @param {string} event The name of the event. TODO: Document all event types + * @param {function} callback The function to call when the event is triggered. + */Terminal.on=on;Terminal.off=off;Terminal.cancel=cancel;module.exports=Terminal; + +},{"./CompositionHelper.js":1,"./EventEmitter.js":2,"./Viewport.js":3,"./handlers/Clipboard.js":4,"./utils/Browser":5}]},{},[7])(7) +}); +//# sourceMappingURL=xterm.js.map diff --git a/vendor/assets/stylesheets/katex.scss b/vendor/assets/stylesheets/katex.scss new file mode 100644 index 0000000000000000000000000000000000000000..9dd8a30bf512c823f9b1d7cbece3795ba0f607cf --- /dev/null +++ b/vendor/assets/stylesheets/katex.scss @@ -0,0 +1,977 @@ +/* +The MIT License (MIT) + +Copyright (c) 2015 Khan Academy + +This software also uses portions of the underscore.js project, which is +MIT licensed with the following copyright: + +Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + Here is how to build a version of KaTeX that works with gitlab. + + The problem is that the standard procedure for changing font location doesn't work for the empty string. + + 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. + 2. make (requires node) + 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss + 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js, + build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and + fonts/* to gitlab/vendor/assets/fonts/. +*/ + +@font-face { + font-family: 'KaTeX_AMS'; + src: url(font-path('KaTeX_AMS-Regular.eot')); + src: url(font-path('KaTeX_AMS-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_AMS-Regular.woff2')) format('woff2'), url(font-path('KaTeX_AMS-Regular.woff')) format('woff'), url(font-path('KaTeX_AMS-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Caligraphic'; + src: url(font-path('KaTeX_Caligraphic-Bold.eot')); + src: url(font-path('KaTeX_Caligraphic-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Bold.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Bold.ttf')) format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Caligraphic'; + src: url(font-path('KaTeX_Caligraphic-Regular.eot')); + src: url(font-path('KaTeX_Caligraphic-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Regular.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Fraktur'; + src: url(font-path('KaTeX_Fraktur-Bold.eot')); + src: url(font-path('KaTeX_Fraktur-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Bold.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Bold.ttf')) format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Fraktur'; + src: url(font-path('KaTeX_Fraktur-Regular.eot')); + src: url(font-path('KaTeX_Fraktur-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Regular.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url(font-path('KaTeX_Main-Bold.eot')); + src: url(font-path('KaTeX_Main-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Main-Bold.woff')) format('woff'), url(font-path('KaTeX_Main-Bold.ttf')) format('truetype'); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url(font-path('KaTeX_Main-Italic.eot')); + src: url(font-path('KaTeX_Main-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Main-Italic.woff')) format('woff'), url(font-path('KaTeX_Main-Italic.ttf')) format('truetype'); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: 'KaTeX_Main'; + src: url(font-path('KaTeX_Main-Regular.eot')); + src: url(font-path('KaTeX_Main-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Main-Regular.woff')) format('woff'), url(font-path('KaTeX_Main-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Math'; + src: url(font-path('KaTeX_Math-Italic.eot')); + src: url(font-path('KaTeX_Math-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Math-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Math-Italic.woff')) format('woff'), url(font-path('KaTeX_Math-Italic.ttf')) format('truetype'); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: 'KaTeX_SansSerif'; + src: url(font-path('KaTeX_SansSerif-Regular.eot')); + src: url(font-path('KaTeX_SansSerif-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_SansSerif-Regular.woff2')) format('woff2'), url(font-path('KaTeX_SansSerif-Regular.woff')) format('woff'), url(font-path('KaTeX_SansSerif-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Script'; + src: url(font-path('KaTeX_Script-Regular.eot')); + src: url(font-path('KaTeX_Script-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Script-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Script-Regular.woff')) format('woff'), url(font-path('KaTeX_Script-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size1'; + src: url(font-path('KaTeX_Size1-Regular.eot')); + src: url(font-path('KaTeX_Size1-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size1-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size1-Regular.woff')) format('woff'), url(font-path('KaTeX_Size1-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size2'; + src: url(font-path('KaTeX_Size2-Regular.eot')); + src: url(font-path('KaTeX_Size2-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size2-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size2-Regular.woff')) format('woff'), url(font-path('KaTeX_Size2-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size3'; + src: url(font-path('KaTeX_Size3-Regular.eot')); + src: url(font-path('KaTeX_Size3-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size3-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size3-Regular.woff')) format('woff'), url(font-path('KaTeX_Size3-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Size4'; + src: url(font-path('KaTeX_Size4-Regular.eot')); + src: url(font-path('KaTeX_Size4-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size4-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size4-Regular.woff')) format('woff'), url(font-path('KaTeX_Size4-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'KaTeX_Typewriter'; + src: url(font-path('KaTeX_Typewriter-Regular.eot')); + src: url(font-path('KaTeX_Typewriter-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Typewriter-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Typewriter-Regular.woff')) format('woff'), url(font-path('KaTeX_Typewriter-Regular.ttf')) format('truetype'); + font-weight: normal; + font-style: normal; +} +.katex-display { + display: block; + margin: 1em 0; + text-align: center; +} +.katex-display > .katex { + display: inline-block; + text-align: initial; +} +.katex { + font: normal 1.21em KaTeX_Main, Times New Roman, serif; + line-height: 1.2; + white-space: nowrap; + text-indent: 0; +} +.katex .katex-html { + display: inline-block; +} +.katex .katex-mathml { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.katex .base { + display: inline-block; +} +.katex .strut { + display: inline-block; +} +.katex .mathit { + font-family: KaTeX_Math; + font-style: italic; +} +.katex .mathbf { + font-family: KaTeX_Main; + font-weight: bold; +} +.katex .amsrm { + font-family: KaTeX_AMS; +} +.katex .mathbb { + font-family: KaTeX_AMS; +} +.katex .mathcal { + font-family: KaTeX_Caligraphic; +} +.katex .mathfrak { + font-family: KaTeX_Fraktur; +} +.katex .mathtt { + font-family: KaTeX_Typewriter; +} +.katex .mathscr { + font-family: KaTeX_Script; +} +.katex .mathsf { + font-family: KaTeX_SansSerif; +} +.katex .mainit { + font-family: KaTeX_Main; + font-style: italic; +} +.katex .textstyle > .mord + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mord + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mord + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mord + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mop + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mbin + .mord { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mop { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mopen { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .minner { + margin-left: 0.22222em; +} +.katex .textstyle > .mrel + .mord { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mop { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mopen { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .minner { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mclose + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mclose + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mrel { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mclose { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .minner + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .minner + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .minner { + margin-left: 0.16667em; +} +.katex .mord + .mop { + margin-left: 0.16667em; +} +.katex .mop + .mord { + margin-left: 0.16667em; +} +.katex .mop + .mop { + margin-left: 0.16667em; +} +.katex .mclose + .mop { + margin-left: 0.16667em; +} +.katex .minner + .mop { + margin-left: 0.16667em; +} +.katex .reset-textstyle.textstyle { + font-size: 1em; +} +.katex .reset-textstyle.scriptstyle { + font-size: 0.7em; +} +.katex .reset-textstyle.scriptscriptstyle { + font-size: 0.5em; +} +.katex .reset-scriptstyle.textstyle { + font-size: 1.42857em; +} +.katex .reset-scriptstyle.scriptstyle { + font-size: 1em; +} +.katex .reset-scriptstyle.scriptscriptstyle { + font-size: 0.71429em; +} +.katex .reset-scriptscriptstyle.textstyle { + font-size: 2em; +} +.katex .reset-scriptscriptstyle.scriptstyle { + font-size: 1.4em; +} +.katex .reset-scriptscriptstyle.scriptscriptstyle { + font-size: 1em; +} +.katex .style-wrap { + position: relative; +} +.katex .vlist { + display: inline-block; +} +.katex .vlist > span { + display: block; + height: 0; + position: relative; +} +.katex .vlist > span > span { + display: inline-block; +} +.katex .vlist .baseline-fix { + display: inline-table; + table-layout: fixed; +} +.katex .msupsub { + text-align: left; +} +.katex .mfrac > span > span { + text-align: center; +} +.katex .mfrac .frac-line { + width: 100%; +} +.katex .mfrac .frac-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .mfrac .frac-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .mspace { + display: inline-block; +} +.katex .mspace.negativethinspace { + margin-left: -0.16667em; +} +.katex .mspace.thinspace { + width: 0.16667em; +} +.katex .mspace.mediumspace { + width: 0.22222em; +} +.katex .mspace.thickspace { + width: 0.27778em; +} +.katex .mspace.enspace { + width: 0.5em; +} +.katex .mspace.quad { + width: 1em; +} +.katex .mspace.qquad { + width: 2em; +} +.katex .llap, +.katex .rlap { + width: 0; + position: relative; +} +.katex .llap > .inner, +.katex .rlap > .inner { + position: absolute; +} +.katex .llap > .fix, +.katex .rlap > .fix { + display: inline-block; +} +.katex .llap > .inner { + right: 0; +} +.katex .rlap > .inner { + left: 0; +} +.katex .katex-logo .a { + font-size: 0.75em; + margin-left: -0.32em; + position: relative; + top: -0.2em; +} +.katex .katex-logo .t { + margin-left: -0.23em; +} +.katex .katex-logo .e { + margin-left: -0.1667em; + position: relative; + top: 0.2155em; +} +.katex .katex-logo .x { + margin-left: -0.125em; +} +.katex .rule { + display: inline-block; + border: solid 0; + position: relative; +} +.katex .overline .overline-line, +.katex .underline .underline-line { + width: 100%; +} +.katex .overline .overline-line:before, +.katex .underline .underline-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .overline .overline-line:after, +.katex .underline .underline-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .sqrt-sign { + position: relative; +} +.katex .sqrt .sqrt-line { + width: 100%; +} +.katex .sqrt .sqrt-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .sqrt .sqrt-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .root { + margin-left: 0.27777778em; + margin-right: -0.55555556em; +} +.katex .sizing, +.katex .fontsize-ensurer { + display: inline-block; +} +.katex .sizing.reset-size1.size1, +.katex .fontsize-ensurer.reset-size1.size1 { + font-size: 1em; +} +.katex .sizing.reset-size1.size2, +.katex .fontsize-ensurer.reset-size1.size2 { + font-size: 1.4em; +} +.katex .sizing.reset-size1.size3, +.katex .fontsize-ensurer.reset-size1.size3 { + font-size: 1.6em; +} +.katex .sizing.reset-size1.size4, +.katex .fontsize-ensurer.reset-size1.size4 { + font-size: 1.8em; +} +.katex .sizing.reset-size1.size5, +.katex .fontsize-ensurer.reset-size1.size5 { + font-size: 2em; +} +.katex .sizing.reset-size1.size6, +.katex .fontsize-ensurer.reset-size1.size6 { + font-size: 2.4em; +} +.katex .sizing.reset-size1.size7, +.katex .fontsize-ensurer.reset-size1.size7 { + font-size: 2.88em; +} +.katex .sizing.reset-size1.size8, +.katex .fontsize-ensurer.reset-size1.size8 { + font-size: 3.46em; +} +.katex .sizing.reset-size1.size9, +.katex .fontsize-ensurer.reset-size1.size9 { + font-size: 4.14em; +} +.katex .sizing.reset-size1.size10, +.katex .fontsize-ensurer.reset-size1.size10 { + font-size: 4.98em; +} +.katex .sizing.reset-size2.size1, +.katex .fontsize-ensurer.reset-size2.size1 { + font-size: 0.71428571em; +} +.katex .sizing.reset-size2.size2, +.katex .fontsize-ensurer.reset-size2.size2 { + font-size: 1em; +} +.katex .sizing.reset-size2.size3, +.katex .fontsize-ensurer.reset-size2.size3 { + font-size: 1.14285714em; +} +.katex .sizing.reset-size2.size4, +.katex .fontsize-ensurer.reset-size2.size4 { + font-size: 1.28571429em; +} +.katex .sizing.reset-size2.size5, +.katex .fontsize-ensurer.reset-size2.size5 { + font-size: 1.42857143em; +} +.katex .sizing.reset-size2.size6, +.katex .fontsize-ensurer.reset-size2.size6 { + font-size: 1.71428571em; +} +.katex .sizing.reset-size2.size7, +.katex .fontsize-ensurer.reset-size2.size7 { + font-size: 2.05714286em; +} +.katex .sizing.reset-size2.size8, +.katex .fontsize-ensurer.reset-size2.size8 { + font-size: 2.47142857em; +} +.katex .sizing.reset-size2.size9, +.katex .fontsize-ensurer.reset-size2.size9 { + font-size: 2.95714286em; +} +.katex .sizing.reset-size2.size10, +.katex .fontsize-ensurer.reset-size2.size10 { + font-size: 3.55714286em; +} +.katex .sizing.reset-size3.size1, +.katex .fontsize-ensurer.reset-size3.size1 { + font-size: 0.625em; +} +.katex .sizing.reset-size3.size2, +.katex .fontsize-ensurer.reset-size3.size2 { + font-size: 0.875em; +} +.katex .sizing.reset-size3.size3, +.katex .fontsize-ensurer.reset-size3.size3 { + font-size: 1em; +} +.katex .sizing.reset-size3.size4, +.katex .fontsize-ensurer.reset-size3.size4 { + font-size: 1.125em; +} +.katex .sizing.reset-size3.size5, +.katex .fontsize-ensurer.reset-size3.size5 { + font-size: 1.25em; +} +.katex .sizing.reset-size3.size6, +.katex .fontsize-ensurer.reset-size3.size6 { + font-size: 1.5em; +} +.katex .sizing.reset-size3.size7, +.katex .fontsize-ensurer.reset-size3.size7 { + font-size: 1.8em; +} +.katex .sizing.reset-size3.size8, +.katex .fontsize-ensurer.reset-size3.size8 { + font-size: 2.1625em; +} +.katex .sizing.reset-size3.size9, +.katex .fontsize-ensurer.reset-size3.size9 { + font-size: 2.5875em; +} +.katex .sizing.reset-size3.size10, +.katex .fontsize-ensurer.reset-size3.size10 { + font-size: 3.1125em; +} +.katex .sizing.reset-size4.size1, +.katex .fontsize-ensurer.reset-size4.size1 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size4.size2, +.katex .fontsize-ensurer.reset-size4.size2 { + font-size: 0.77777778em; +} +.katex .sizing.reset-size4.size3, +.katex .fontsize-ensurer.reset-size4.size3 { + font-size: 0.88888889em; +} +.katex .sizing.reset-size4.size4, +.katex .fontsize-ensurer.reset-size4.size4 { + font-size: 1em; +} +.katex .sizing.reset-size4.size5, +.katex .fontsize-ensurer.reset-size4.size5 { + font-size: 1.11111111em; +} +.katex .sizing.reset-size4.size6, +.katex .fontsize-ensurer.reset-size4.size6 { + font-size: 1.33333333em; +} +.katex .sizing.reset-size4.size7, +.katex .fontsize-ensurer.reset-size4.size7 { + font-size: 1.6em; +} +.katex .sizing.reset-size4.size8, +.katex .fontsize-ensurer.reset-size4.size8 { + font-size: 1.92222222em; +} +.katex .sizing.reset-size4.size9, +.katex .fontsize-ensurer.reset-size4.size9 { + font-size: 2.3em; +} +.katex .sizing.reset-size4.size10, +.katex .fontsize-ensurer.reset-size4.size10 { + font-size: 2.76666667em; +} +.katex .sizing.reset-size5.size1, +.katex .fontsize-ensurer.reset-size5.size1 { + font-size: 0.5em; +} +.katex .sizing.reset-size5.size2, +.katex .fontsize-ensurer.reset-size5.size2 { + font-size: 0.7em; +} +.katex .sizing.reset-size5.size3, +.katex .fontsize-ensurer.reset-size5.size3 { + font-size: 0.8em; +} +.katex .sizing.reset-size5.size4, +.katex .fontsize-ensurer.reset-size5.size4 { + font-size: 0.9em; +} +.katex .sizing.reset-size5.size5, +.katex .fontsize-ensurer.reset-size5.size5 { + font-size: 1em; +} +.katex .sizing.reset-size5.size6, +.katex .fontsize-ensurer.reset-size5.size6 { + font-size: 1.2em; +} +.katex .sizing.reset-size5.size7, +.katex .fontsize-ensurer.reset-size5.size7 { + font-size: 1.44em; +} +.katex .sizing.reset-size5.size8, +.katex .fontsize-ensurer.reset-size5.size8 { + font-size: 1.73em; +} +.katex .sizing.reset-size5.size9, +.katex .fontsize-ensurer.reset-size5.size9 { + font-size: 2.07em; +} +.katex .sizing.reset-size5.size10, +.katex .fontsize-ensurer.reset-size5.size10 { + font-size: 2.49em; +} +.katex .sizing.reset-size6.size1, +.katex .fontsize-ensurer.reset-size6.size1 { + font-size: 0.41666667em; +} +.katex .sizing.reset-size6.size2, +.katex .fontsize-ensurer.reset-size6.size2 { + font-size: 0.58333333em; +} +.katex .sizing.reset-size6.size3, +.katex .fontsize-ensurer.reset-size6.size3 { + font-size: 0.66666667em; +} +.katex .sizing.reset-size6.size4, +.katex .fontsize-ensurer.reset-size6.size4 { + font-size: 0.75em; +} +.katex .sizing.reset-size6.size5, +.katex .fontsize-ensurer.reset-size6.size5 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size6.size6, +.katex .fontsize-ensurer.reset-size6.size6 { + font-size: 1em; +} +.katex .sizing.reset-size6.size7, +.katex .fontsize-ensurer.reset-size6.size7 { + font-size: 1.2em; +} +.katex .sizing.reset-size6.size8, +.katex .fontsize-ensurer.reset-size6.size8 { + font-size: 1.44166667em; +} +.katex .sizing.reset-size6.size9, +.katex .fontsize-ensurer.reset-size6.size9 { + font-size: 1.725em; +} +.katex .sizing.reset-size6.size10, +.katex .fontsize-ensurer.reset-size6.size10 { + font-size: 2.075em; +} +.katex .sizing.reset-size7.size1, +.katex .fontsize-ensurer.reset-size7.size1 { + font-size: 0.34722222em; +} +.katex .sizing.reset-size7.size2, +.katex .fontsize-ensurer.reset-size7.size2 { + font-size: 0.48611111em; +} +.katex .sizing.reset-size7.size3, +.katex .fontsize-ensurer.reset-size7.size3 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size7.size4, +.katex .fontsize-ensurer.reset-size7.size4 { + font-size: 0.625em; +} +.katex .sizing.reset-size7.size5, +.katex .fontsize-ensurer.reset-size7.size5 { + font-size: 0.69444444em; +} +.katex .sizing.reset-size7.size6, +.katex .fontsize-ensurer.reset-size7.size6 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size7.size7, +.katex .fontsize-ensurer.reset-size7.size7 { + font-size: 1em; +} +.katex .sizing.reset-size7.size8, +.katex .fontsize-ensurer.reset-size7.size8 { + font-size: 1.20138889em; +} +.katex .sizing.reset-size7.size9, +.katex .fontsize-ensurer.reset-size7.size9 { + font-size: 1.4375em; +} +.katex .sizing.reset-size7.size10, +.katex .fontsize-ensurer.reset-size7.size10 { + font-size: 1.72916667em; +} +.katex .sizing.reset-size8.size1, +.katex .fontsize-ensurer.reset-size8.size1 { + font-size: 0.28901734em; +} +.katex .sizing.reset-size8.size2, +.katex .fontsize-ensurer.reset-size8.size2 { + font-size: 0.40462428em; +} +.katex .sizing.reset-size8.size3, +.katex .fontsize-ensurer.reset-size8.size3 { + font-size: 0.46242775em; +} +.katex .sizing.reset-size8.size4, +.katex .fontsize-ensurer.reset-size8.size4 { + font-size: 0.52023121em; +} +.katex .sizing.reset-size8.size5, +.katex .fontsize-ensurer.reset-size8.size5 { + font-size: 0.57803468em; +} +.katex .sizing.reset-size8.size6, +.katex .fontsize-ensurer.reset-size8.size6 { + font-size: 0.69364162em; +} +.katex .sizing.reset-size8.size7, +.katex .fontsize-ensurer.reset-size8.size7 { + font-size: 0.83236994em; +} +.katex .sizing.reset-size8.size8, +.katex .fontsize-ensurer.reset-size8.size8 { + font-size: 1em; +} +.katex .sizing.reset-size8.size9, +.katex .fontsize-ensurer.reset-size8.size9 { + font-size: 1.19653179em; +} +.katex .sizing.reset-size8.size10, +.katex .fontsize-ensurer.reset-size8.size10 { + font-size: 1.43930636em; +} +.katex .sizing.reset-size9.size1, +.katex .fontsize-ensurer.reset-size9.size1 { + font-size: 0.24154589em; +} +.katex .sizing.reset-size9.size2, +.katex .fontsize-ensurer.reset-size9.size2 { + font-size: 0.33816425em; +} +.katex .sizing.reset-size9.size3, +.katex .fontsize-ensurer.reset-size9.size3 { + font-size: 0.38647343em; +} +.katex .sizing.reset-size9.size4, +.katex .fontsize-ensurer.reset-size9.size4 { + font-size: 0.43478261em; +} +.katex .sizing.reset-size9.size5, +.katex .fontsize-ensurer.reset-size9.size5 { + font-size: 0.48309179em; +} +.katex .sizing.reset-size9.size6, +.katex .fontsize-ensurer.reset-size9.size6 { + font-size: 0.57971014em; +} +.katex .sizing.reset-size9.size7, +.katex .fontsize-ensurer.reset-size9.size7 { + font-size: 0.69565217em; +} +.katex .sizing.reset-size9.size8, +.katex .fontsize-ensurer.reset-size9.size8 { + font-size: 0.83574879em; +} +.katex .sizing.reset-size9.size9, +.katex .fontsize-ensurer.reset-size9.size9 { + font-size: 1em; +} +.katex .sizing.reset-size9.size10, +.katex .fontsize-ensurer.reset-size9.size10 { + font-size: 1.20289855em; +} +.katex .sizing.reset-size10.size1, +.katex .fontsize-ensurer.reset-size10.size1 { + font-size: 0.20080321em; +} +.katex .sizing.reset-size10.size2, +.katex .fontsize-ensurer.reset-size10.size2 { + font-size: 0.2811245em; +} +.katex .sizing.reset-size10.size3, +.katex .fontsize-ensurer.reset-size10.size3 { + font-size: 0.32128514em; +} +.katex .sizing.reset-size10.size4, +.katex .fontsize-ensurer.reset-size10.size4 { + font-size: 0.36144578em; +} +.katex .sizing.reset-size10.size5, +.katex .fontsize-ensurer.reset-size10.size5 { + font-size: 0.40160643em; +} +.katex .sizing.reset-size10.size6, +.katex .fontsize-ensurer.reset-size10.size6 { + font-size: 0.48192771em; +} +.katex .sizing.reset-size10.size7, +.katex .fontsize-ensurer.reset-size10.size7 { + font-size: 0.57831325em; +} +.katex .sizing.reset-size10.size8, +.katex .fontsize-ensurer.reset-size10.size8 { + font-size: 0.69477912em; +} +.katex .sizing.reset-size10.size9, +.katex .fontsize-ensurer.reset-size10.size9 { + font-size: 0.8313253em; +} +.katex .sizing.reset-size10.size10, +.katex .fontsize-ensurer.reset-size10.size10 { + font-size: 1em; +} +.katex .delimsizing.size1 { + font-family: KaTeX_Size1; +} +.katex .delimsizing.size2 { + font-family: KaTeX_Size2; +} +.katex .delimsizing.size3 { + font-family: KaTeX_Size3; +} +.katex .delimsizing.size4 { + font-family: KaTeX_Size4; +} +.katex .delimsizing.mult .delim-size1 > span { + font-family: KaTeX_Size1; +} +.katex .delimsizing.mult .delim-size4 > span { + font-family: KaTeX_Size4; +} +.katex .nulldelimiter { + display: inline-block; + width: 0.12em; +} +.katex .op-symbol { + position: relative; +} +.katex .op-symbol.small-op { + font-family: KaTeX_Size1; +} +.katex .op-symbol.large-op { + font-family: KaTeX_Size2; +} +.katex .op-limits > .vlist > span { + text-align: center; +} +.katex .accent > .vlist > span { + text-align: center; +} +.katex .accent .accent-body > span { + width: 0; +} +.katex .accent .accent-body.accent-vec > span { + position: relative; + left: 0.326em; +} +.katex .mtable .vertical-separator { + display: inline-block; + margin: 0 -0.025em; + border-right: 0.05em solid black; +} +.katex .mtable .arraycolsep { + display: inline-block; +} +.katex .mtable .col-align-c > .vlist { + text-align: center; +} +.katex .mtable .col-align-l > .vlist { + text-align: left; +} +.katex .mtable .col-align-r > .vlist { + text-align: right; +} diff --git a/vendor/assets/stylesheets/xterm/xterm.css b/vendor/assets/stylesheets/xterm/xterm.css new file mode 100644 index 0000000000000000000000000000000000000000..b30d7b493f11b42b618dc1d3109f0b32c9414a78 --- /dev/null +++ b/vendor/assets/stylesheets/xterm/xterm.css @@ -0,0 +1,2206 @@ +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +/* + * Default style for xterm.js + */ + +.terminal { + background-color: #000; + color: #fff; + font-family: courier-new, courier, monospace; + font-feature-settings: "liga" 0; + position: relative; +} + +.terminal.focus, +.terminal:focus { + outline: none; +} + +.terminal .xterm-helpers { + position: absolute; + top: 0; +} + +.terminal .xterm-helper-textarea { + /* + * HACK: to fix IE's blinking cursor + * Move textarea out of the screen to the far left, so that the cursor is not visible. + */ + position: absolute; + opacity: 0; + left: -9999em; + top: -9999em; + width: 0; + height: 0; + z-index: -10; + /** Prevent wrapping so the IME appears against the textarea at the correct position */ + white-space: nowrap; + overflow: hidden; + resize: none; +} + +.terminal .terminal-cursor { + background-color: #fff; + color: #000; +} + +.terminal:not(.focus) .terminal-cursor { + outline: 1px solid #fff; + outline-offset: -1px; + background-color: transparent; +} + +.terminal.focus .terminal-cursor.blinking { + animation: blink-cursor 1.2s infinite step-end; +} + +@keyframes blink-cursor { + 0% { + background-color: #fff; + color: #000; + } + 50% { + background-color: transparent; + color: #FFF; + } +} + +.terminal .composition-view { + background: #000; + color: #FFF; + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} + +.terminal .composition-view.active { + display: block; +} + +.terminal .xterm-viewport { + /* On OS X this is required in order for the scroll bar to appear fully opaque */ + background-color: #000; + overflow-y: scroll; +} + +.terminal .xterm-rows { + position: absolute; + left: 0; + top: 0; +} + +.terminal .xterm-rows > div { + /* Lines containing spans and text nodes ocassionally wrap despite being the same width (#327) */ + white-space: nowrap; +} + +.terminal .xterm-scroll-area { + visibility: hidden; +} + +.terminal .xterm-char-measure-element { + display: inline-block; + visibility: hidden; + position: absolute; + left: -9999em; +} + +/* + * Determine default colors for xterm.js + */ +.terminal .xterm-bold { + font-weight: bold; +} + +.terminal .xterm-underline { + text-decoration: underline; +} + +.terminal .xterm-blink { + text-decoration: blink; +} + +.terminal .xterm-hidden { + visibility: hidden; +} + +.terminal .xterm-color-0 { + color: #2e3436; +} + +.terminal .xterm-bg-color-0 { + background-color: #2e3436; +} + +.terminal .xterm-color-1 { + color: #cc0000; +} + +.terminal .xterm-bg-color-1 { + background-color: #cc0000; +} + +.terminal .xterm-color-2 { + color: #4e9a06; +} + +.terminal .xterm-bg-color-2 { + background-color: #4e9a06; +} + +.terminal .xterm-color-3 { + color: #c4a000; +} + +.terminal .xterm-bg-color-3 { + background-color: #c4a000; +} + +.terminal .xterm-color-4 { + color: #3465a4; +} + +.terminal .xterm-bg-color-4 { + background-color: #3465a4; +} + +.terminal .xterm-color-5 { + color: #75507b; +} + +.terminal .xterm-bg-color-5 { + background-color: #75507b; +} + +.terminal .xterm-color-6 { + color: #06989a; +} + +.terminal .xterm-bg-color-6 { + background-color: #06989a; +} + +.terminal .xterm-color-7 { + color: #d3d7cf; +} + +.terminal .xterm-bg-color-7 { + background-color: #d3d7cf; +} + +.terminal .xterm-color-8 { + color: #555753; +} + +.terminal .xterm-bg-color-8 { + background-color: #555753; +} + +.terminal .xterm-color-9 { + color: #ef2929; +} + +.terminal .xterm-bg-color-9 { + background-color: #ef2929; +} + +.terminal .xterm-color-10 { + color: #8ae234; +} + +.terminal .xterm-bg-color-10 { + background-color: #8ae234; +} + +.terminal .xterm-color-11 { + color: #fce94f; +} + +.terminal .xterm-bg-color-11 { + background-color: #fce94f; +} + +.terminal .xterm-color-12 { + color: #729fcf; +} + +.terminal .xterm-bg-color-12 { + background-color: #729fcf; +} + +.terminal .xterm-color-13 { + color: #ad7fa8; +} + +.terminal .xterm-bg-color-13 { + background-color: #ad7fa8; +} + +.terminal .xterm-color-14 { + color: #34e2e2; +} + +.terminal .xterm-bg-color-14 { + background-color: #34e2e2; +} + +.terminal .xterm-color-15 { + color: #eeeeec; +} + +.terminal .xterm-bg-color-15 { + background-color: #eeeeec; +} + +.terminal .xterm-color-16 { + color: #000000; +} + +.terminal .xterm-bg-color-16 { + background-color: #000000; +} + +.terminal .xterm-color-17 { + color: #00005f; +} + +.terminal .xterm-bg-color-17 { + background-color: #00005f; +} + +.terminal .xterm-color-18 { + color: #000087; +} + +.terminal .xterm-bg-color-18 { + background-color: #000087; +} + +.terminal .xterm-color-19 { + color: #0000af; +} + +.terminal .xterm-bg-color-19 { + background-color: #0000af; +} + +.terminal .xterm-color-20 { + color: #0000d7; +} + +.terminal .xterm-bg-color-20 { + background-color: #0000d7; +} + +.terminal .xterm-color-21 { + color: #0000ff; +} + +.terminal .xterm-bg-color-21 { + background-color: #0000ff; +} + +.terminal .xterm-color-22 { + color: #005f00; +} + +.terminal .xterm-bg-color-22 { + background-color: #005f00; +} + +.terminal .xterm-color-23 { + color: #005f5f; +} + +.terminal .xterm-bg-color-23 { + background-color: #005f5f; +} + +.terminal .xterm-color-24 { + color: #005f87; +} + +.terminal .xterm-bg-color-24 { + background-color: #005f87; +} + +.terminal .xterm-color-25 { + color: #005faf; +} + +.terminal .xterm-bg-color-25 { + background-color: #005faf; +} + +.terminal .xterm-color-26 { + color: #005fd7; +} + +.terminal .xterm-bg-color-26 { + background-color: #005fd7; +} + +.terminal .xterm-color-27 { + color: #005fff; +} + +.terminal .xterm-bg-color-27 { + background-color: #005fff; +} + +.terminal .xterm-color-28 { + color: #008700; +} + +.terminal .xterm-bg-color-28 { + background-color: #008700; +} + +.terminal .xterm-color-29 { + color: #00875f; +} + +.terminal .xterm-bg-color-29 { + background-color: #00875f; +} + +.terminal .xterm-color-30 { + color: #008787; +} + +.terminal .xterm-bg-color-30 { + background-color: #008787; +} + +.terminal .xterm-color-31 { + color: #0087af; +} + +.terminal .xterm-bg-color-31 { + background-color: #0087af; +} + +.terminal .xterm-color-32 { + color: #0087d7; +} + +.terminal .xterm-bg-color-32 { + background-color: #0087d7; +} + +.terminal .xterm-color-33 { + color: #0087ff; +} + +.terminal .xterm-bg-color-33 { + background-color: #0087ff; +} + +.terminal .xterm-color-34 { + color: #00af00; +} + +.terminal .xterm-bg-color-34 { + background-color: #00af00; +} + +.terminal .xterm-color-35 { + color: #00af5f; +} + +.terminal .xterm-bg-color-35 { + background-color: #00af5f; +} + +.terminal .xterm-color-36 { + color: #00af87; +} + +.terminal .xterm-bg-color-36 { + background-color: #00af87; +} + +.terminal .xterm-color-37 { + color: #00afaf; +} + +.terminal .xterm-bg-color-37 { + background-color: #00afaf; +} + +.terminal .xterm-color-38 { + color: #00afd7; +} + +.terminal .xterm-bg-color-38 { + background-color: #00afd7; +} + +.terminal .xterm-color-39 { + color: #00afff; +} + +.terminal .xterm-bg-color-39 { + background-color: #00afff; +} + +.terminal .xterm-color-40 { + color: #00d700; +} + +.terminal .xterm-bg-color-40 { + background-color: #00d700; +} + +.terminal .xterm-color-41 { + color: #00d75f; +} + +.terminal .xterm-bg-color-41 { + background-color: #00d75f; +} + +.terminal .xterm-color-42 { + color: #00d787; +} + +.terminal .xterm-bg-color-42 { + background-color: #00d787; +} + +.terminal .xterm-color-43 { + color: #00d7af; +} + +.terminal .xterm-bg-color-43 { + background-color: #00d7af; +} + +.terminal .xterm-color-44 { + color: #00d7d7; +} + +.terminal .xterm-bg-color-44 { + background-color: #00d7d7; +} + +.terminal .xterm-color-45 { + color: #00d7ff; +} + +.terminal .xterm-bg-color-45 { + background-color: #00d7ff; +} + +.terminal .xterm-color-46 { + color: #00ff00; +} + +.terminal .xterm-bg-color-46 { + background-color: #00ff00; +} + +.terminal .xterm-color-47 { + color: #00ff5f; +} + +.terminal .xterm-bg-color-47 { + background-color: #00ff5f; +} + +.terminal .xterm-color-48 { + color: #00ff87; +} + +.terminal .xterm-bg-color-48 { + background-color: #00ff87; +} + +.terminal .xterm-color-49 { + color: #00ffaf; +} + +.terminal .xterm-bg-color-49 { + background-color: #00ffaf; +} + +.terminal .xterm-color-50 { + color: #00ffd7; +} + +.terminal .xterm-bg-color-50 { + background-color: #00ffd7; +} + +.terminal .xterm-color-51 { + color: #00ffff; +} + +.terminal .xterm-bg-color-51 { + background-color: #00ffff; +} + +.terminal .xterm-color-52 { + color: #5f0000; +} + +.terminal .xterm-bg-color-52 { + background-color: #5f0000; +} + +.terminal .xterm-color-53 { + color: #5f005f; +} + +.terminal .xterm-bg-color-53 { + background-color: #5f005f; +} + +.terminal .xterm-color-54 { + color: #5f0087; +} + +.terminal .xterm-bg-color-54 { + background-color: #5f0087; +} + +.terminal .xterm-color-55 { + color: #5f00af; +} + +.terminal .xterm-bg-color-55 { + background-color: #5f00af; +} + +.terminal .xterm-color-56 { + color: #5f00d7; +} + +.terminal .xterm-bg-color-56 { + background-color: #5f00d7; +} + +.terminal .xterm-color-57 { + color: #5f00ff; +} + +.terminal .xterm-bg-color-57 { + background-color: #5f00ff; +} + +.terminal .xterm-color-58 { + color: #5f5f00; +} + +.terminal .xterm-bg-color-58 { + background-color: #5f5f00; +} + +.terminal .xterm-color-59 { + color: #5f5f5f; +} + +.terminal .xterm-bg-color-59 { + background-color: #5f5f5f; +} + +.terminal .xterm-color-60 { + color: #5f5f87; +} + +.terminal .xterm-bg-color-60 { + background-color: #5f5f87; +} + +.terminal .xterm-color-61 { + color: #5f5faf; +} + +.terminal .xterm-bg-color-61 { + background-color: #5f5faf; +} + +.terminal .xterm-color-62 { + color: #5f5fd7; +} + +.terminal .xterm-bg-color-62 { + background-color: #5f5fd7; +} + +.terminal .xterm-color-63 { + color: #5f5fff; +} + +.terminal .xterm-bg-color-63 { + background-color: #5f5fff; +} + +.terminal .xterm-color-64 { + color: #5f8700; +} + +.terminal .xterm-bg-color-64 { + background-color: #5f8700; +} + +.terminal .xterm-color-65 { + color: #5f875f; +} + +.terminal .xterm-bg-color-65 { + background-color: #5f875f; +} + +.terminal .xterm-color-66 { + color: #5f8787; +} + +.terminal .xterm-bg-color-66 { + background-color: #5f8787; +} + +.terminal .xterm-color-67 { + color: #5f87af; +} + +.terminal .xterm-bg-color-67 { + background-color: #5f87af; +} + +.terminal .xterm-color-68 { + color: #5f87d7; +} + +.terminal .xterm-bg-color-68 { + background-color: #5f87d7; +} + +.terminal .xterm-color-69 { + color: #5f87ff; +} + +.terminal .xterm-bg-color-69 { + background-color: #5f87ff; +} + +.terminal .xterm-color-70 { + color: #5faf00; +} + +.terminal .xterm-bg-color-70 { + background-color: #5faf00; +} + +.terminal .xterm-color-71 { + color: #5faf5f; +} + +.terminal .xterm-bg-color-71 { + background-color: #5faf5f; +} + +.terminal .xterm-color-72 { + color: #5faf87; +} + +.terminal .xterm-bg-color-72 { + background-color: #5faf87; +} + +.terminal .xterm-color-73 { + color: #5fafaf; +} + +.terminal .xterm-bg-color-73 { + background-color: #5fafaf; +} + +.terminal .xterm-color-74 { + color: #5fafd7; +} + +.terminal .xterm-bg-color-74 { + background-color: #5fafd7; +} + +.terminal .xterm-color-75 { + color: #5fafff; +} + +.terminal .xterm-bg-color-75 { + background-color: #5fafff; +} + +.terminal .xterm-color-76 { + color: #5fd700; +} + +.terminal .xterm-bg-color-76 { + background-color: #5fd700; +} + +.terminal .xterm-color-77 { + color: #5fd75f; +} + +.terminal .xterm-bg-color-77 { + background-color: #5fd75f; +} + +.terminal .xterm-color-78 { + color: #5fd787; +} + +.terminal .xterm-bg-color-78 { + background-color: #5fd787; +} + +.terminal .xterm-color-79 { + color: #5fd7af; +} + +.terminal .xterm-bg-color-79 { + background-color: #5fd7af; +} + +.terminal .xterm-color-80 { + color: #5fd7d7; +} + +.terminal .xterm-bg-color-80 { + background-color: #5fd7d7; +} + +.terminal .xterm-color-81 { + color: #5fd7ff; +} + +.terminal .xterm-bg-color-81 { + background-color: #5fd7ff; +} + +.terminal .xterm-color-82 { + color: #5fff00; +} + +.terminal .xterm-bg-color-82 { + background-color: #5fff00; +} + +.terminal .xterm-color-83 { + color: #5fff5f; +} + +.terminal .xterm-bg-color-83 { + background-color: #5fff5f; +} + +.terminal .xterm-color-84 { + color: #5fff87; +} + +.terminal .xterm-bg-color-84 { + background-color: #5fff87; +} + +.terminal .xterm-color-85 { + color: #5fffaf; +} + +.terminal .xterm-bg-color-85 { + background-color: #5fffaf; +} + +.terminal .xterm-color-86 { + color: #5fffd7; +} + +.terminal .xterm-bg-color-86 { + background-color: #5fffd7; +} + +.terminal .xterm-color-87 { + color: #5fffff; +} + +.terminal .xterm-bg-color-87 { + background-color: #5fffff; +} + +.terminal .xterm-color-88 { + color: #870000; +} + +.terminal .xterm-bg-color-88 { + background-color: #870000; +} + +.terminal .xterm-color-89 { + color: #87005f; +} + +.terminal .xterm-bg-color-89 { + background-color: #87005f; +} + +.terminal .xterm-color-90 { + color: #870087; +} + +.terminal .xterm-bg-color-90 { + background-color: #870087; +} + +.terminal .xterm-color-91 { + color: #8700af; +} + +.terminal .xterm-bg-color-91 { + background-color: #8700af; +} + +.terminal .xterm-color-92 { + color: #8700d7; +} + +.terminal .xterm-bg-color-92 { + background-color: #8700d7; +} + +.terminal .xterm-color-93 { + color: #8700ff; +} + +.terminal .xterm-bg-color-93 { + background-color: #8700ff; +} + +.terminal .xterm-color-94 { + color: #875f00; +} + +.terminal .xterm-bg-color-94 { + background-color: #875f00; +} + +.terminal .xterm-color-95 { + color: #875f5f; +} + +.terminal .xterm-bg-color-95 { + background-color: #875f5f; +} + +.terminal .xterm-color-96 { + color: #875f87; +} + +.terminal .xterm-bg-color-96 { + background-color: #875f87; +} + +.terminal .xterm-color-97 { + color: #875faf; +} + +.terminal .xterm-bg-color-97 { + background-color: #875faf; +} + +.terminal .xterm-color-98 { + color: #875fd7; +} + +.terminal .xterm-bg-color-98 { + background-color: #875fd7; +} + +.terminal .xterm-color-99 { + color: #875fff; +} + +.terminal .xterm-bg-color-99 { + background-color: #875fff; +} + +.terminal .xterm-color-100 { + color: #878700; +} + +.terminal .xterm-bg-color-100 { + background-color: #878700; +} + +.terminal .xterm-color-101 { + color: #87875f; +} + +.terminal .xterm-bg-color-101 { + background-color: #87875f; +} + +.terminal .xterm-color-102 { + color: #878787; +} + +.terminal .xterm-bg-color-102 { + background-color: #878787; +} + +.terminal .xterm-color-103 { + color: #8787af; +} + +.terminal .xterm-bg-color-103 { + background-color: #8787af; +} + +.terminal .xterm-color-104 { + color: #8787d7; +} + +.terminal .xterm-bg-color-104 { + background-color: #8787d7; +} + +.terminal .xterm-color-105 { + color: #8787ff; +} + +.terminal .xterm-bg-color-105 { + background-color: #8787ff; +} + +.terminal .xterm-color-106 { + color: #87af00; +} + +.terminal .xterm-bg-color-106 { + background-color: #87af00; +} + +.terminal .xterm-color-107 { + color: #87af5f; +} + +.terminal .xterm-bg-color-107 { + background-color: #87af5f; +} + +.terminal .xterm-color-108 { + color: #87af87; +} + +.terminal .xterm-bg-color-108 { + background-color: #87af87; +} + +.terminal .xterm-color-109 { + color: #87afaf; +} + +.terminal .xterm-bg-color-109 { + background-color: #87afaf; +} + +.terminal .xterm-color-110 { + color: #87afd7; +} + +.terminal .xterm-bg-color-110 { + background-color: #87afd7; +} + +.terminal .xterm-color-111 { + color: #87afff; +} + +.terminal .xterm-bg-color-111 { + background-color: #87afff; +} + +.terminal .xterm-color-112 { + color: #87d700; +} + +.terminal .xterm-bg-color-112 { + background-color: #87d700; +} + +.terminal .xterm-color-113 { + color: #87d75f; +} + +.terminal .xterm-bg-color-113 { + background-color: #87d75f; +} + +.terminal .xterm-color-114 { + color: #87d787; +} + +.terminal .xterm-bg-color-114 { + background-color: #87d787; +} + +.terminal .xterm-color-115 { + color: #87d7af; +} + +.terminal .xterm-bg-color-115 { + background-color: #87d7af; +} + +.terminal .xterm-color-116 { + color: #87d7d7; +} + +.terminal .xterm-bg-color-116 { + background-color: #87d7d7; +} + +.terminal .xterm-color-117 { + color: #87d7ff; +} + +.terminal .xterm-bg-color-117 { + background-color: #87d7ff; +} + +.terminal .xterm-color-118 { + color: #87ff00; +} + +.terminal .xterm-bg-color-118 { + background-color: #87ff00; +} + +.terminal .xterm-color-119 { + color: #87ff5f; +} + +.terminal .xterm-bg-color-119 { + background-color: #87ff5f; +} + +.terminal .xterm-color-120 { + color: #87ff87; +} + +.terminal .xterm-bg-color-120 { + background-color: #87ff87; +} + +.terminal .xterm-color-121 { + color: #87ffaf; +} + +.terminal .xterm-bg-color-121 { + background-color: #87ffaf; +} + +.terminal .xterm-color-122 { + color: #87ffd7; +} + +.terminal .xterm-bg-color-122 { + background-color: #87ffd7; +} + +.terminal .xterm-color-123 { + color: #87ffff; +} + +.terminal .xterm-bg-color-123 { + background-color: #87ffff; +} + +.terminal .xterm-color-124 { + color: #af0000; +} + +.terminal .xterm-bg-color-124 { + background-color: #af0000; +} + +.terminal .xterm-color-125 { + color: #af005f; +} + +.terminal .xterm-bg-color-125 { + background-color: #af005f; +} + +.terminal .xterm-color-126 { + color: #af0087; +} + +.terminal .xterm-bg-color-126 { + background-color: #af0087; +} + +.terminal .xterm-color-127 { + color: #af00af; +} + +.terminal .xterm-bg-color-127 { + background-color: #af00af; +} + +.terminal .xterm-color-128 { + color: #af00d7; +} + +.terminal .xterm-bg-color-128 { + background-color: #af00d7; +} + +.terminal .xterm-color-129 { + color: #af00ff; +} + +.terminal .xterm-bg-color-129 { + background-color: #af00ff; +} + +.terminal .xterm-color-130 { + color: #af5f00; +} + +.terminal .xterm-bg-color-130 { + background-color: #af5f00; +} + +.terminal .xterm-color-131 { + color: #af5f5f; +} + +.terminal .xterm-bg-color-131 { + background-color: #af5f5f; +} + +.terminal .xterm-color-132 { + color: #af5f87; +} + +.terminal .xterm-bg-color-132 { + background-color: #af5f87; +} + +.terminal .xterm-color-133 { + color: #af5faf; +} + +.terminal .xterm-bg-color-133 { + background-color: #af5faf; +} + +.terminal .xterm-color-134 { + color: #af5fd7; +} + +.terminal .xterm-bg-color-134 { + background-color: #af5fd7; +} + +.terminal .xterm-color-135 { + color: #af5fff; +} + +.terminal .xterm-bg-color-135 { + background-color: #af5fff; +} + +.terminal .xterm-color-136 { + color: #af8700; +} + +.terminal .xterm-bg-color-136 { + background-color: #af8700; +} + +.terminal .xterm-color-137 { + color: #af875f; +} + +.terminal .xterm-bg-color-137 { + background-color: #af875f; +} + +.terminal .xterm-color-138 { + color: #af8787; +} + +.terminal .xterm-bg-color-138 { + background-color: #af8787; +} + +.terminal .xterm-color-139 { + color: #af87af; +} + +.terminal .xterm-bg-color-139 { + background-color: #af87af; +} + +.terminal .xterm-color-140 { + color: #af87d7; +} + +.terminal .xterm-bg-color-140 { + background-color: #af87d7; +} + +.terminal .xterm-color-141 { + color: #af87ff; +} + +.terminal .xterm-bg-color-141 { + background-color: #af87ff; +} + +.terminal .xterm-color-142 { + color: #afaf00; +} + +.terminal .xterm-bg-color-142 { + background-color: #afaf00; +} + +.terminal .xterm-color-143 { + color: #afaf5f; +} + +.terminal .xterm-bg-color-143 { + background-color: #afaf5f; +} + +.terminal .xterm-color-144 { + color: #afaf87; +} + +.terminal .xterm-bg-color-144 { + background-color: #afaf87; +} + +.terminal .xterm-color-145 { + color: #afafaf; +} + +.terminal .xterm-bg-color-145 { + background-color: #afafaf; +} + +.terminal .xterm-color-146 { + color: #afafd7; +} + +.terminal .xterm-bg-color-146 { + background-color: #afafd7; +} + +.terminal .xterm-color-147 { + color: #afafff; +} + +.terminal .xterm-bg-color-147 { + background-color: #afafff; +} + +.terminal .xterm-color-148 { + color: #afd700; +} + +.terminal .xterm-bg-color-148 { + background-color: #afd700; +} + +.terminal .xterm-color-149 { + color: #afd75f; +} + +.terminal .xterm-bg-color-149 { + background-color: #afd75f; +} + +.terminal .xterm-color-150 { + color: #afd787; +} + +.terminal .xterm-bg-color-150 { + background-color: #afd787; +} + +.terminal .xterm-color-151 { + color: #afd7af; +} + +.terminal .xterm-bg-color-151 { + background-color: #afd7af; +} + +.terminal .xterm-color-152 { + color: #afd7d7; +} + +.terminal .xterm-bg-color-152 { + background-color: #afd7d7; +} + +.terminal .xterm-color-153 { + color: #afd7ff; +} + +.terminal .xterm-bg-color-153 { + background-color: #afd7ff; +} + +.terminal .xterm-color-154 { + color: #afff00; +} + +.terminal .xterm-bg-color-154 { + background-color: #afff00; +} + +.terminal .xterm-color-155 { + color: #afff5f; +} + +.terminal .xterm-bg-color-155 { + background-color: #afff5f; +} + +.terminal .xterm-color-156 { + color: #afff87; +} + +.terminal .xterm-bg-color-156 { + background-color: #afff87; +} + +.terminal .xterm-color-157 { + color: #afffaf; +} + +.terminal .xterm-bg-color-157 { + background-color: #afffaf; +} + +.terminal .xterm-color-158 { + color: #afffd7; +} + +.terminal .xterm-bg-color-158 { + background-color: #afffd7; +} + +.terminal .xterm-color-159 { + color: #afffff; +} + +.terminal .xterm-bg-color-159 { + background-color: #afffff; +} + +.terminal .xterm-color-160 { + color: #d70000; +} + +.terminal .xterm-bg-color-160 { + background-color: #d70000; +} + +.terminal .xterm-color-161 { + color: #d7005f; +} + +.terminal .xterm-bg-color-161 { + background-color: #d7005f; +} + +.terminal .xterm-color-162 { + color: #d70087; +} + +.terminal .xterm-bg-color-162 { + background-color: #d70087; +} + +.terminal .xterm-color-163 { + color: #d700af; +} + +.terminal .xterm-bg-color-163 { + background-color: #d700af; +} + +.terminal .xterm-color-164 { + color: #d700d7; +} + +.terminal .xterm-bg-color-164 { + background-color: #d700d7; +} + +.terminal .xterm-color-165 { + color: #d700ff; +} + +.terminal .xterm-bg-color-165 { + background-color: #d700ff; +} + +.terminal .xterm-color-166 { + color: #d75f00; +} + +.terminal .xterm-bg-color-166 { + background-color: #d75f00; +} + +.terminal .xterm-color-167 { + color: #d75f5f; +} + +.terminal .xterm-bg-color-167 { + background-color: #d75f5f; +} + +.terminal .xterm-color-168 { + color: #d75f87; +} + +.terminal .xterm-bg-color-168 { + background-color: #d75f87; +} + +.terminal .xterm-color-169 { + color: #d75faf; +} + +.terminal .xterm-bg-color-169 { + background-color: #d75faf; +} + +.terminal .xterm-color-170 { + color: #d75fd7; +} + +.terminal .xterm-bg-color-170 { + background-color: #d75fd7; +} + +.terminal .xterm-color-171 { + color: #d75fff; +} + +.terminal .xterm-bg-color-171 { + background-color: #d75fff; +} + +.terminal .xterm-color-172 { + color: #d78700; +} + +.terminal .xterm-bg-color-172 { + background-color: #d78700; +} + +.terminal .xterm-color-173 { + color: #d7875f; +} + +.terminal .xterm-bg-color-173 { + background-color: #d7875f; +} + +.terminal .xterm-color-174 { + color: #d78787; +} + +.terminal .xterm-bg-color-174 { + background-color: #d78787; +} + +.terminal .xterm-color-175 { + color: #d787af; +} + +.terminal .xterm-bg-color-175 { + background-color: #d787af; +} + +.terminal .xterm-color-176 { + color: #d787d7; +} + +.terminal .xterm-bg-color-176 { + background-color: #d787d7; +} + +.terminal .xterm-color-177 { + color: #d787ff; +} + +.terminal .xterm-bg-color-177 { + background-color: #d787ff; +} + +.terminal .xterm-color-178 { + color: #d7af00; +} + +.terminal .xterm-bg-color-178 { + background-color: #d7af00; +} + +.terminal .xterm-color-179 { + color: #d7af5f; +} + +.terminal .xterm-bg-color-179 { + background-color: #d7af5f; +} + +.terminal .xterm-color-180 { + color: #d7af87; +} + +.terminal .xterm-bg-color-180 { + background-color: #d7af87; +} + +.terminal .xterm-color-181 { + color: #d7afaf; +} + +.terminal .xterm-bg-color-181 { + background-color: #d7afaf; +} + +.terminal .xterm-color-182 { + color: #d7afd7; +} + +.terminal .xterm-bg-color-182 { + background-color: #d7afd7; +} + +.terminal .xterm-color-183 { + color: #d7afff; +} + +.terminal .xterm-bg-color-183 { + background-color: #d7afff; +} + +.terminal .xterm-color-184 { + color: #d7d700; +} + +.terminal .xterm-bg-color-184 { + background-color: #d7d700; +} + +.terminal .xterm-color-185 { + color: #d7d75f; +} + +.terminal .xterm-bg-color-185 { + background-color: #d7d75f; +} + +.terminal .xterm-color-186 { + color: #d7d787; +} + +.terminal .xterm-bg-color-186 { + background-color: #d7d787; +} + +.terminal .xterm-color-187 { + color: #d7d7af; +} + +.terminal .xterm-bg-color-187 { + background-color: #d7d7af; +} + +.terminal .xterm-color-188 { + color: #d7d7d7; +} + +.terminal .xterm-bg-color-188 { + background-color: #d7d7d7; +} + +.terminal .xterm-color-189 { + color: #d7d7ff; +} + +.terminal .xterm-bg-color-189 { + background-color: #d7d7ff; +} + +.terminal .xterm-color-190 { + color: #d7ff00; +} + +.terminal .xterm-bg-color-190 { + background-color: #d7ff00; +} + +.terminal .xterm-color-191 { + color: #d7ff5f; +} + +.terminal .xterm-bg-color-191 { + background-color: #d7ff5f; +} + +.terminal .xterm-color-192 { + color: #d7ff87; +} + +.terminal .xterm-bg-color-192 { + background-color: #d7ff87; +} + +.terminal .xterm-color-193 { + color: #d7ffaf; +} + +.terminal .xterm-bg-color-193 { + background-color: #d7ffaf; +} + +.terminal .xterm-color-194 { + color: #d7ffd7; +} + +.terminal .xterm-bg-color-194 { + background-color: #d7ffd7; +} + +.terminal .xterm-color-195 { + color: #d7ffff; +} + +.terminal .xterm-bg-color-195 { + background-color: #d7ffff; +} + +.terminal .xterm-color-196 { + color: #ff0000; +} + +.terminal .xterm-bg-color-196 { + background-color: #ff0000; +} + +.terminal .xterm-color-197 { + color: #ff005f; +} + +.terminal .xterm-bg-color-197 { + background-color: #ff005f; +} + +.terminal .xterm-color-198 { + color: #ff0087; +} + +.terminal .xterm-bg-color-198 { + background-color: #ff0087; +} + +.terminal .xterm-color-199 { + color: #ff00af; +} + +.terminal .xterm-bg-color-199 { + background-color: #ff00af; +} + +.terminal .xterm-color-200 { + color: #ff00d7; +} + +.terminal .xterm-bg-color-200 { + background-color: #ff00d7; +} + +.terminal .xterm-color-201 { + color: #ff00ff; +} + +.terminal .xterm-bg-color-201 { + background-color: #ff00ff; +} + +.terminal .xterm-color-202 { + color: #ff5f00; +} + +.terminal .xterm-bg-color-202 { + background-color: #ff5f00; +} + +.terminal .xterm-color-203 { + color: #ff5f5f; +} + +.terminal .xterm-bg-color-203 { + background-color: #ff5f5f; +} + +.terminal .xterm-color-204 { + color: #ff5f87; +} + +.terminal .xterm-bg-color-204 { + background-color: #ff5f87; +} + +.terminal .xterm-color-205 { + color: #ff5faf; +} + +.terminal .xterm-bg-color-205 { + background-color: #ff5faf; +} + +.terminal .xterm-color-206 { + color: #ff5fd7; +} + +.terminal .xterm-bg-color-206 { + background-color: #ff5fd7; +} + +.terminal .xterm-color-207 { + color: #ff5fff; +} + +.terminal .xterm-bg-color-207 { + background-color: #ff5fff; +} + +.terminal .xterm-color-208 { + color: #ff8700; +} + +.terminal .xterm-bg-color-208 { + background-color: #ff8700; +} + +.terminal .xterm-color-209 { + color: #ff875f; +} + +.terminal .xterm-bg-color-209 { + background-color: #ff875f; +} + +.terminal .xterm-color-210 { + color: #ff8787; +} + +.terminal .xterm-bg-color-210 { + background-color: #ff8787; +} + +.terminal .xterm-color-211 { + color: #ff87af; +} + +.terminal .xterm-bg-color-211 { + background-color: #ff87af; +} + +.terminal .xterm-color-212 { + color: #ff87d7; +} + +.terminal .xterm-bg-color-212 { + background-color: #ff87d7; +} + +.terminal .xterm-color-213 { + color: #ff87ff; +} + +.terminal .xterm-bg-color-213 { + background-color: #ff87ff; +} + +.terminal .xterm-color-214 { + color: #ffaf00; +} + +.terminal .xterm-bg-color-214 { + background-color: #ffaf00; +} + +.terminal .xterm-color-215 { + color: #ffaf5f; +} + +.terminal .xterm-bg-color-215 { + background-color: #ffaf5f; +} + +.terminal .xterm-color-216 { + color: #ffaf87; +} + +.terminal .xterm-bg-color-216 { + background-color: #ffaf87; +} + +.terminal .xterm-color-217 { + color: #ffafaf; +} + +.terminal .xterm-bg-color-217 { + background-color: #ffafaf; +} + +.terminal .xterm-color-218 { + color: #ffafd7; +} + +.terminal .xterm-bg-color-218 { + background-color: #ffafd7; +} + +.terminal .xterm-color-219 { + color: #ffafff; +} + +.terminal .xterm-bg-color-219 { + background-color: #ffafff; +} + +.terminal .xterm-color-220 { + color: #ffd700; +} + +.terminal .xterm-bg-color-220 { + background-color: #ffd700; +} + +.terminal .xterm-color-221 { + color: #ffd75f; +} + +.terminal .xterm-bg-color-221 { + background-color: #ffd75f; +} + +.terminal .xterm-color-222 { + color: #ffd787; +} + +.terminal .xterm-bg-color-222 { + background-color: #ffd787; +} + +.terminal .xterm-color-223 { + color: #ffd7af; +} + +.terminal .xterm-bg-color-223 { + background-color: #ffd7af; +} + +.terminal .xterm-color-224 { + color: #ffd7d7; +} + +.terminal .xterm-bg-color-224 { + background-color: #ffd7d7; +} + +.terminal .xterm-color-225 { + color: #ffd7ff; +} + +.terminal .xterm-bg-color-225 { + background-color: #ffd7ff; +} + +.terminal .xterm-color-226 { + color: #ffff00; +} + +.terminal .xterm-bg-color-226 { + background-color: #ffff00; +} + +.terminal .xterm-color-227 { + color: #ffff5f; +} + +.terminal .xterm-bg-color-227 { + background-color: #ffff5f; +} + +.terminal .xterm-color-228 { + color: #ffff87; +} + +.terminal .xterm-bg-color-228 { + background-color: #ffff87; +} + +.terminal .xterm-color-229 { + color: #ffffaf; +} + +.terminal .xterm-bg-color-229 { + background-color: #ffffaf; +} + +.terminal .xterm-color-230 { + color: #ffffd7; +} + +.terminal .xterm-bg-color-230 { + background-color: #ffffd7; +} + +.terminal .xterm-color-231 { + color: #ffffff; +} + +.terminal .xterm-bg-color-231 { + background-color: #ffffff; +} + +.terminal .xterm-color-232 { + color: #080808; +} + +.terminal .xterm-bg-color-232 { + background-color: #080808; +} + +.terminal .xterm-color-233 { + color: #121212; +} + +.terminal .xterm-bg-color-233 { + background-color: #121212; +} + +.terminal .xterm-color-234 { + color: #1c1c1c; +} + +.terminal .xterm-bg-color-234 { + background-color: #1c1c1c; +} + +.terminal .xterm-color-235 { + color: #262626; +} + +.terminal .xterm-bg-color-235 { + background-color: #262626; +} + +.terminal .xterm-color-236 { + color: #303030; +} + +.terminal .xterm-bg-color-236 { + background-color: #303030; +} + +.terminal .xterm-color-237 { + color: #3a3a3a; +} + +.terminal .xterm-bg-color-237 { + background-color: #3a3a3a; +} + +.terminal .xterm-color-238 { + color: #444444; +} + +.terminal .xterm-bg-color-238 { + background-color: #444444; +} + +.terminal .xterm-color-239 { + color: #4e4e4e; +} + +.terminal .xterm-bg-color-239 { + background-color: #4e4e4e; +} + +.terminal .xterm-color-240 { + color: #585858; +} + +.terminal .xterm-bg-color-240 { + background-color: #585858; +} + +.terminal .xterm-color-241 { + color: #626262; +} + +.terminal .xterm-bg-color-241 { + background-color: #626262; +} + +.terminal .xterm-color-242 { + color: #6c6c6c; +} + +.terminal .xterm-bg-color-242 { + background-color: #6c6c6c; +} + +.terminal .xterm-color-243 { + color: #767676; +} + +.terminal .xterm-bg-color-243 { + background-color: #767676; +} + +.terminal .xterm-color-244 { + color: #808080; +} + +.terminal .xterm-bg-color-244 { + background-color: #808080; +} + +.terminal .xterm-color-245 { + color: #8a8a8a; +} + +.terminal .xterm-bg-color-245 { + background-color: #8a8a8a; +} + +.terminal .xterm-color-246 { + color: #949494; +} + +.terminal .xterm-bg-color-246 { + background-color: #949494; +} + +.terminal .xterm-color-247 { + color: #9e9e9e; +} + +.terminal .xterm-bg-color-247 { + background-color: #9e9e9e; +} + +.terminal .xterm-color-248 { + color: #a8a8a8; +} + +.terminal .xterm-bg-color-248 { + background-color: #a8a8a8; +} + +.terminal .xterm-color-249 { + color: #b2b2b2; +} + +.terminal .xterm-bg-color-249 { + background-color: #b2b2b2; +} + +.terminal .xterm-color-250 { + color: #bcbcbc; +} + +.terminal .xterm-bg-color-250 { + background-color: #bcbcbc; +} + +.terminal .xterm-color-251 { + color: #c6c6c6; +} + +.terminal .xterm-bg-color-251 { + background-color: #c6c6c6; +} + +.terminal .xterm-color-252 { + color: #d0d0d0; +} + +.terminal .xterm-bg-color-252 { + background-color: #d0d0d0; +} + +.terminal .xterm-color-253 { + color: #dadada; +} + +.terminal .xterm-bg-color-253 { + background-color: #dadada; +} + +.terminal .xterm-color-254 { + color: #e4e4e4; +} + +.terminal .xterm-bg-color-254 { + background-color: #e4e4e4; +} + +.terminal .xterm-color-255 { + color: #eeeeee; +} + +.terminal .xterm-bg-color-255 { + background-color: #eeeeee; +} diff --git a/vendor/dockerfile/HTTPdDockerfile b/vendor/dockerfile/HTTPdDockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2f05427323c29c19355093ef1a8d2772b4a18971 --- /dev/null +++ b/vendor/dockerfile/HTTPdDockerfile @@ -0,0 +1,3 @@ +FROM httpd:alpine + +COPY ./ /usr/local/apache2/htdocs/ diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index 935ceef0680bdc5d33b9be9117d60300098a8b51..d028d1251ad8b8c356a13b3b1873b9e0d852caa1 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -35,6 +35,8 @@ captures/ # Intellij *.iml .idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml .idea/libraries # Keystore files diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore index 1e9158e2a85f8972b26a901be4be6e1972088885..e3923f96fcef253e4cf660f41def2118533ce5ab 100644 --- a/vendor/gitignore/Autotools.gitignore +++ b/vendor/gitignore/Autotools.gitignore @@ -1,6 +1,11 @@ # http://www.gnu.org/software/automake Makefile.in +/ar-lib +/mdate-sh +/py-compile +/test-driver +/ylwrap # http://www.gnu.org/software/autoconf @@ -9,10 +14,20 @@ Makefile.in /autoscan-*.log /aclocal.m4 /compile +/config.guess /config.h.in +/config.sub /configure /configure.scan /depcomp /install-sh /missing /stamp-h1 + +# https://www.gnu.org/software/libtool/ + +/ltmain.sh + +# http://www.gnu.org/software/texinfo + +/texinfo.tex diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore index 0cc7e4b527553e3b57cd57050a58d7a560675c69..27ada0591ece3b36728bc22ec184035ea99a1f1b 100644 --- a/vendor/gitignore/CMake.gitignore +++ b/vendor/gitignore/CMake.gitignore @@ -4,4 +4,5 @@ CMakeScripts Makefile cmake_install.cmake install_manifest.txt +compile_commands.json CTestTestfile.cmake diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore index 0f77d9e1d175c90e6048385f689e35830506bc23..60571a0c38300fdcff4e2259600bc924db52a50b 100644 --- a/vendor/gitignore/CodeIgniter.gitignore +++ b/vendor/gitignore/CodeIgniter.gitignore @@ -4,3 +4,8 @@ */cache/* !*/cache/index.html !*/cache/.htaccess + +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore index 4806e580b601c8ac5f557c98c5320af98638020f..e7de127b014060bb3414971121e2fa7a9d7edb54 100644 --- a/vendor/gitignore/CommonLisp.gitignore +++ b/vendor/gitignore/CommonLisp.gitignore @@ -1,3 +1,17 @@ *.FASL *.fasl *.lisp-temp +*.dfsl +*.pfsl +*.d64fsl +*.p64fsl +*.lx64fsl +*.lx32fsl +*.dx64fsl +*.dx32fsl +*.fx64fsl +*.fx32fsl +*.sx64fsl +*.sx32fsl +*.wx64fsl +*.wx32fsl diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore index d3083b3a605d974e7af5060029a58012d71c251b..f25a61d9964771127aba75e69c46e661a9809b73 100644 --- a/vendor/gitignore/Coq.gitignore +++ b/vendor/gitignore/Coq.gitignore @@ -1,3 +1,30 @@ -*.vo +.*.aux +*.a +*.cma +*.cmi +*.cmo +*.cmx +*.cmxa +*.cmxs *.glob +*.ml.d +*.ml4.d +*.mli.d +*.mllib.d +*.mlpack.d +*.native +*.o *.v.d +*.vio +*.vo +.coq-native/ +.csdp.cache +.lia.cache +.nia.cache +.nlia.cache +.nra.cache +csdp.cache +lia.cache +nia.cache +nlia.cache +nra.cache diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore index 7c280441649860d99e9df07c398b32012ea8fffd..4b366585ddc72341c8724e47d7542ab6c05a52e9 100644 --- a/vendor/gitignore/Dart.gitignore +++ b/vendor/gitignore/Dart.gitignore @@ -1,13 +1,19 @@ # See https://www.dartlang.org/tools/private-files.html # Files and directories created by pub -.buildlog + +# SDK 1.20 and later (no longer creates packages directories) .packages -.project .pub/ build/ + +# Older SDK versions +# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20) +.project +.buildlog **/packages/ + # Files created by dart2js # (Most Dart developers will use pub build to compile Dart, use/modify these # rules if you intend to use dart2js directly diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore index 9b4291b7fe84bd3c7ec2b4a483280a06f5bbebf5..206569dc66128fcd5cad0cbff0ba6de54ea48030 100644 --- a/vendor/gitignore/Elisp.gitignore +++ b/vendor/gitignore/Elisp.gitignore @@ -3,3 +3,9 @@ # Packaging .cask + +# Backup files +*~ + +# Undo-tree save-files +*.~undo-tree diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore index 755b605549d85e74d41a649a0ce9bad1516bcf08..ac67aaf32434c95828f40870a5066804d8ad166c 100644 --- a/vendor/gitignore/Elixir.gitignore +++ b/vendor/gitignore/Elixir.gitignore @@ -3,3 +3,4 @@ /deps erl_crash.dump *.ez +*.beam diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore index 0c96c9ad0601bb5abd2fe8a696dc09b6de378920..3ac7904dcd22376c0897f236e22a7f47af821d06 100644 --- a/vendor/gitignore/Global/Emacs.gitignore +++ b/vendor/gitignore/Global/Emacs.gitignore @@ -39,4 +39,7 @@ flycheck_*.el /server/ # projectiles files -.projectile \ No newline at end of file +.projectile + +# directory configuration +.dir-locals.el diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore deleted file mode 100644 index 27c13510bf5ef442fd309edcb71bb48139fdebfb..0000000000000000000000000000000000000000 --- a/vendor/gitignore/Global/IPythonNotebook.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Temporary data -.ipynb_checkpoints/ diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index 0a254147875173d9bf25903d5e1d837b6cdd3561..e375c744b6d4f6a9fe4b37d9ed020c14d0645aed 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -6,6 +6,7 @@ .idea/tasks.xml # Sensitive or high-churn files: +.idea/dataSources/ .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore index 69c8c2b29ce59a0ee4b1a19934460006a21b59b8..95ff2244c990f144c262a76386a8ea73616b1a81 100644 --- a/vendor/gitignore/Global/SublimeText.gitignore +++ b/vendor/gitignore/Global/SublimeText.gitignore @@ -20,6 +20,9 @@ Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore index bdc04a0b529b192db15e4a7a98450d94e5c5501d..42e7afc100512b21af0a3339fe851d21e3ee4ed5 100644 --- a/vendor/gitignore/Global/Vim.gitignore +++ b/vendor/gitignore/Global/Vim.gitignore @@ -1,6 +1,8 @@ # swap -[._]*.s[a-w][a-z] -[._]s[a-w][a-z] +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] # session Session.vim # temporary diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore index d9960081c9820bd3ac26d6d798e18191bafba5f2..0511e2b51f0d42d1dff69f4ed5df03c6649ca356 100644 --- a/vendor/gitignore/Global/VisualStudioCode.gitignore +++ b/vendor/gitignore/Global/VisualStudioCode.gitignore @@ -2,3 +2,4 @@ !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json +!.vscode/extensions.json diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore index a0d31452b0e29226c83da01d538f29dc8f338e73..ba26afd9653949ea6f5fdef25505f05bdb7d200f 100644 --- a/vendor/gitignore/Global/Windows.gitignore +++ b/vendor/gitignore/Global/Windows.gitignore @@ -1,6 +1,7 @@ -# Windows image file caches +# Windows thumbnail cache files Thumbs.db ehthumbs.db +ehthumbs_vista.db # Folder config file Desktop.ini diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index 397a0ed4acb63ede454a58674a75984d0ba18308..5e1047c9d78472f78ba178b0fed1de964f14a3d8 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -26,5 +26,5 @@ _testmain.go # Output of the go coverage tool, specifically when used with LiteIDE *.out -# external packages folder +# External packages folder vendor/ diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index 32858aad3c383ed1ff0a0f9bdf231d54a00c9e88..e44e086040597dcb2d46b3f299863bf1ef9790ae 100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore @@ -1,5 +1,8 @@ *.class +# BlueJ files +*.ctxt + # Mobile Tools for Java (J2ME) .mtj.tmp/ diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore index e7c594fa3e28d5dc5e1ce08c936b456023c61c09..a2d1564060b9a92a69e4e536063a72420675449c 100644 --- a/vendor/gitignore/Laravel.gitignore +++ b/vendor/gitignore/Laravel.gitignore @@ -6,8 +6,8 @@ bootstrap/compiled.php app/storage/ # Laravel 5 & Lumen specific -bootstrap/cache/ public/storage +storage/*.key .env.*.php .env.php .env diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore index 1cdc9f7fd45e8003ad15bea0eaf7c76cd09c8e42..9af45b175ae8a1640c37d2795dcb8f16cdbe9a35 100644 --- a/vendor/gitignore/Maven.gitignore +++ b/vendor/gitignore/Maven.gitignore @@ -7,3 +7,6 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties + +# Exclude maven wrapper +!/.mvn/wrapper/maven-wrapper.jar diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index bc7fc55724c152c385ba6264ca6bf4ebf2e680fb..9a439fcd988ce41c4a86c5377045d0cbbe1244c2 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -42,3 +42,7 @@ jspm_packages # Output of 'npm pack' *.tgz + +# Yarn Integrity file +.yarn-integrity + diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore index ae2ad536abbc81c49dad4aca4f308277032a7ad0..d41364ab18efc3ace28f9182b55cbf1e58e9d4fb 100644 --- a/vendor/gitignore/Perl.gitignore +++ b/vendor/gitignore/Perl.gitignore @@ -1,20 +1,34 @@ -/blib/ -/.build/ -_build/ -cover_db/ -inc/ -Build !Build/ -Build.bat .last_cover_stats -/Makefile -/Makefile.old -/MANIFEST.bak /META.yml /META.json /MYMETA.* -nytprof.out -/pm_to_blib *.o *.bs + +# Devel::Cover +cover_db/ + +# Devel::NYTProf +nytprof.out + +# Dizt::Zilla +/.build/ + +# Module::Build +_build/ +Build +Build.bat + +# Module::Install +inc/ + +# ExtUitls::MakeMaker +/blib/ /_eumm/ +/*.gz +/Makefile +/Makefile.old +/MANIFEST.bak +/pm_to_blib +/*.zip diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index 6a2bf47ade9c4dd6746a6ceaee6b68c8ce1ce883..9a05e2debe51c5a55ea528b513e3dd9c1c33c0ae 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -20,6 +20,7 @@ lib64/ parts/ sdist/ var/ +wheels/ *.egg-info/ .installed.cfg *.egg diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore index 7d56f982f81f90b8502d42688408750a7afd736c..ed4d3c6c28dcecca9c235c55dee4421b21307a97 100644 --- a/vendor/gitignore/Symfony.gitignore +++ b/vendor/gitignore/Symfony.gitignore @@ -31,9 +31,6 @@ /web/bundles/ /web/uploads/ -# Assets managed by Bower -/web/assets/vendor/ - # PHPUnit /app/phpunit.xml /phpunit.xml @@ -45,4 +42,4 @@ /composer.phar # Backup entities generated with doctrine:generate:entities command -*/Entity/*~ +**/Entity/*~ diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index 1afbaf197f4c08bf0969488219ce50cb9084bcec..69bfb1eec3e5e8b95b5af3aa52f522e0b9b2ab25 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -52,12 +52,22 @@ acs-*.bib # beamer *.nav +*.pre *.snm *.vrb +# changes +*.soc + # cprotect *.cpt +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + # fixme *.lox @@ -123,9 +133,7 @@ acs-*.bib *.maf *.mlf *.mlt -*.mtc -*.mtc[0-9] -*.mtc[1-9][0-9] +*.mtc[0-9]* # minted _minted* @@ -140,6 +148,9 @@ _minted* # nomencl *.nlo +# pax +*.pax + # sagetex *.sagetex.sage *.sagetex.py @@ -202,5 +213,8 @@ TSWLatexianTemp* # KBibTeX *~[0-9]* -# auto folder when using emacs and auctex +# auto folder when using emacs and auctex /auto/* + +# expex forward references with \gathertags +*-tags.tex diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 09e407344ca83384d7122ec296e7b2abe42176f4..d9e876cfcdd91a6c468a503406a0c1d9dc950a08 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -8,7 +8,6 @@ *.user *.userosscache *.sln.docstates -*.vcxproj.filters # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -43,11 +42,11 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# DNX +# .NET Core project.lock.json project.fragment.lock.json artifacts/ -Properties/launchSettings.json +**/Properties/launchSettings.json *_i.c *_p.c diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml index f3fa3949656176b4332b6a5b31fb4c17cb693788..8c5905799346c7d03cf1f27093fb38ef579f6e9f 100644 --- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml @@ -7,6 +7,7 @@ services: build: stage: build script: + - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-') - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY - - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" . - - docker push "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" + - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" . + - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG" diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml index 00f9541e89bc9c0629bd4547d74f37a56ba0e843..981a77497e20f51a93d2d074649a3418c5ce6d19 100644 --- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml @@ -1,6 +1,4 @@ -# This template uses the non default language docker image -# The image already has Hex installed. You might want to consider to use `elixir:latest` -image: trenpixster/elixir:latest +image: elixir:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. @@ -11,6 +9,8 @@ services: - postgres:latest before_script: + - mix local.rebar --force + - mix local.hex --force - mix deps.get mix: diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..e23b6e212f050313d2f03876212e2679ddc7e3fe --- /dev/null +++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml @@ -0,0 +1,37 @@ +image: golang:latest + +# The problem is that to be able to use go get, one needs to put +# the repository in the $GOPATH. So for example if your gitlab domain +# is mydomainperso.com, and that your repository is repos/projectname, and +# the default GOPATH being /go, then you'd need to have your +# repository in /go/src/mydomainperso.com/repos/projectname +# Thus, making a symbolic link corrects this. +before_script: + - ln -s /builds /go/src/mydomainperso.com + - cd /go/src/mydomainperso.com/repos/projectname + +stages: + - test + - build + +format: + stage: test + script: + # Add here all the dependencies, or use glide/govendor to get + # them automatically. + # - curl https://glide.sh/get | sh + - go get github.com/alecthomas/kingpin + - go tool vet -composites=false -shadow=true *.go + - go test -race $(go list ./... | grep -v /vendor/) + +compile: + stage: build + script: + # Add here all the dependencies, or use glide/govendor/... + # to get them automatically. + - go get github.com/alecthomas/kingpin + # Better put this in a Makefile + - go build -race -ldflags "-extldflags '-static'" -o mybinary + artifacts: + paths: + - mybinary diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml index 263c4c199990adfd08cf21daca5253b0f84f3406..98d3039ad06d9097c003d0cadf34ecc9d8356b13 100644 --- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml @@ -31,4 +31,4 @@ build: test: stage: test script: - - ./gradlew -g /cache./gradle check + - ./gradlew -g /cache/.gradle check diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..2ba5cad96822157fdedc14615a0e8f38d7dfbf36 --- /dev/null +++ b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml @@ -0,0 +1,92 @@ +# This file is a template, and might need editing before it works on your project. +image: ayufan/openshift-cli + +stages: + - test + - review + - staging + - production + +variables: + OPENSHIFT_SERVER: openshift.default.svc.cluster.local + # OPENSHIFT_DOMAIN: apps.example.com + # Configure this variable in Secure Variables: + # OPENSHIFT_TOKEN: my.openshift.token + +test1: + stage: test + before_script: [] + script: + - echo run tests + +test2: + stage: test + before_script: [] + script: + - echo run tests + +.deploy: &deploy + before_script: + - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify + - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME" + script: + - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker" + - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow" + - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST" + +review: + <<: *deploy + stage: review + variables: + APP: $CI_BUILD_REF_NAME + APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + on_stop: stop-review + only: + - branches + except: + - master + +stop-review: + <<: *deploy + stage: review + script: + - oc delete all -l "app=$APP" + when: manual + variables: + APP: $CI_BUILD_REF_NAME + GIT_STRATEGY: none + environment: + name: review/$CI_BUILD_REF_NAME + action: stop + only: + - branches + except: + - master + +staging: + <<: *deploy + stage: staging + variables: + APP: staging + APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN + environment: + name: staging + url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN + only: + - master + +production: + <<: *deploy + stage: production + variables: + APP: production + APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN + when: manual + environment: + name: production + url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN + only: + - master diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..249adbc9f4ae93afa59cdab285f6a1616370b116 --- /dev/null +++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml @@ -0,0 +1,76 @@ +# Explaination on the scripts: +# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md +image: registry.gitlab.com/gitlab-examples/openshift-deploy + +variables: + # Application deployment domain + KUBE_DOMAIN: domain.example.com + +stages: + - build + - test + - review + - staging + - production + +build: + stage: build + script: + - command build + only: + - branches + +production: + stage: production + variables: + CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN + script: + - command deploy + environment: + name: production + url: http://production.$KUBE_DOMAIN + when: manual + only: + - master + +staging: + stage: staging + variables: + CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN + script: + - command deploy + environment: + name: staging + url: http://staging.$KUBE_DOMAIN + only: + - master + +review: + stage: review + variables: + CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + script: + - command deploy + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + on_stop: stop_review + only: + - branches + except: + - master + +stop_review: + stage: review + variables: + GIT_STRATEGY: none + script: + - command destroy + environment: + name: review/$CI_BUILD_REF_NAME + action: stop + when: manual + only: + - branches + except: + - master