diff options
514 files changed, 9937 insertions, 4382 deletions
diff --git a/.csscomb.json b/.csscomb.json index 741cc1488b5..aa6a17f7517 100644 --- a/.csscomb.json +++ b/.csscomb.json @@ -6,7 +6,7 @@ "always-semicolon": true, "color-case": "lower", "block-indent": " ", - "color-shorthand": true, + "color-shorthand": false, "element-case": "lower", "space-before-colon": "", "space-after-colon": " ", diff --git a/.flayignore b/.flayignore index f120de527bd..44df2ba2371 100644 --- a/.flayignore +++ b/.flayignore @@ -1,2 +1,3 @@ *.erb lib/gitlab/sanitizers/svg/whitelist.rb +lib/gitlab/diff/position_tracer.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3f3873e57c1..5d2fad03f19 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,8 @@ -image: "ruby:2.3.1" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1" cache: key: "ruby-231" paths: - - vendor/apt - vendor/ruby variables: @@ -141,14 +140,13 @@ spinach 9 10: *spinach-knapsack # Execute all testing suites against Ruby 2.1 .ruby-21: &ruby-21 - image: "ruby:2.1" + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1" <<: *use-db only: - master cache: key: "ruby21" paths: - - vendor/apt - vendor/ruby .rspec-knapsack-ruby21: &rspec-knapsack-ruby21 @@ -209,9 +207,6 @@ rubocop: *exec rake haml_lint: *exec rake scss_lint: *exec rake brakeman: *exec -rake flog: - <<: *exec - allow_failure: yes rake flay: <<: *exec allow_failure: yes diff --git a/.rubocop.yml b/.rubocop.yml index 5bd31ccf329..bec2464c740 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -453,6 +453,10 @@ Style/VariableName: EnforcedStyle: snake_case Enabled: true +# Use the configured style when numbering variables. +Style/VariableNumber: + Enabled: false + # Use when x then ... for one-line cases. Style/WhenThen: Enabled: true @@ -639,6 +643,10 @@ Lint/RescueException: Lint/ShadowedException: Enabled: false +# Checks for Object#to_s usage in string interpolation. +Lint/StringConversionInInterpolation: + Enabled: true + # Do not use prefix `_` for a variable that is used. Lint/UnderscorePrefixedVariableName: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 87520c67dd5..11b34fafa2a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,21 +1,21 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 0` -# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.0. +# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.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: 158 +# Offense count: 160 Lint/AmbiguousRegexpLiteral: Enabled: false -# Offense count: 41 +# Offense count: 40 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Enabled: false -# Offense count: 16 +# Offense count: 18 Lint/HandleExceptions: Enabled: false @@ -23,16 +23,21 @@ Lint/HandleExceptions: Lint/Loop: Enabled: false -# Offense count: 16 +# Offense count: 19 Lint/ShadowingOuterLocalVariable: Enabled: false -# Offense count: 6 +# Offense count: 9 +# Cop supports --auto-correct. +Lint/UnifiedInteger: + Enabled: false + +# Offense count: 13 # Cop supports --auto-correct. -Lint/StringConversionInInterpolation: +Lint/UnneededSplatExpansion: Enabled: false -# Offense count: 49 +# Offense count: 69 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: @@ -44,32 +49,81 @@ Lint/UnusedBlockArgument: Lint/UnusedMethodArgument: Enabled: false -# Offense count: 9 -# Cop supports --auto-correct. -Performance/PushSplat: - Enabled: false - # Offense count: 2 # Cop supports --auto-correct. Performance/RedundantBlockCall: Enabled: false -# Offense count: 4 +# Offense count: 5 # Cop supports --auto-correct. Performance/RedundantMatch: Enabled: false -# Offense count: 27 +# Offense count: 26 # Cop supports --auto-correct. # Configuration parameters: MaxKeyValuePairs. Performance/RedundantMerge: Enabled: false -# Offense count: 61 +# Offense count: 7 +RSpec/BeEql: + Enabled: false + +# Offense count: 20 +# Configuration parameters: CustomIncludeMethods. +RSpec/EmptyExampleGroup: + Enabled: false + +# Offense count: 16 +RSpec/ExpectActual: + Enabled: false + +# Offense count: 34 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: implicit, each, example +RSpec/HookArgument: + Enabled: false + +# Offense count: 168 +RSpec/LeadingSubject: + Enabled: false + +# Offense count: 162 +RSpec/LetSetup: + Enabled: false + +# Offense count: 10 +RSpec/MessageChain: + Enabled: false + +# Offense count: 714 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: allow, expect +RSpec/MessageExpectation: + Enabled: false + +# Offense count: 2423 +RSpec/MultipleExpectations: + Max: 36 + +# Offense count: 1504 +RSpec/NamedSubject: + Enabled: false + +# Offense count: 1335 +# Configuration parameters: MaxNesting. +RSpec/NestedGroups: + Enabled: false + +# Offense count: 99 +RSpec/SubjectStub: + Enabled: false + +# Offense count: 64 Rails/OutputSafety: Enabled: false -# Offense count: 129 +# Offense count: 151 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: strict, flexible Rails/TimeZone: @@ -82,58 +136,63 @@ Rails/TimeZone: Rails/Validation: Enabled: false -# Offense count: 273 +# Offense count: 2 +# Cop supports --auto-correct. +Security/JSONLoad: + Enabled: false + +# Offense count: 284 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation Style/AlignParameters: Enabled: false -# Offense count: 30 +# Offense count: 28 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: always, conditionals Style/AndOr: Enabled: false -# Offense count: 50 +# Offense count: 52 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Enabled: false -# Offense count: 289 +# Offense count: 291 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent Style/BracesAroundHashParameters: Enabled: false -# Offense count: 5 +# Offense count: 6 Style/CaseEquality: Enabled: false -# Offense count: 19 +# Offense count: 26 # Cop supports --auto-correct. Style/ColonMethodCall: Enabled: false -# Offense count: 3 +# Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: Keywords. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW Style/CommentAnnotation: Enabled: false -# Offense count: 33 +# Offense count: 30 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Enabled: false -# Offense count: 881 +# Offense count: 957 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: leading, trailing @@ -144,12 +203,12 @@ Style/DotPosition: Style/DoubleNegation: Enabled: false -# Offense count: 4 +# Offense count: 6 # Cop supports --auto-correct. Style/EachWithObject: Enabled: false -# Offense count: 25 +# Offense count: 26 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: empty, nil, both @@ -161,24 +220,24 @@ Style/EmptyElse: Style/EmptyLiteral: Enabled: false -# Offense count: 135 +# Offense count: 140 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Style/ExtraSpacing: Enabled: false -# Offense count: 7 +# Offense count: 6 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: format, sprintf, percent Style/FormatString: Enabled: false -# Offense count: 51 +# Offense count: 201 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false -# Offense count: 9 +# Offense count: 11 Style/IfInsideElse: Enabled: false @@ -188,21 +247,21 @@ Style/IfInsideElse: Style/IfUnlessModifier: Enabled: false -# Offense count: 52 +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets Style/IndentArray: Enabled: false -# Offense count: 97 +# Offense count: 95 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Style/IndentHash: Enabled: false -# Offense count: 12 +# Offense count: 29 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: line_count_dependent, lambda, literal @@ -214,7 +273,7 @@ Style/Lambda: Style/LineEndConcatenation: Enabled: false -# Offense count: 13 +# Offense count: 15 # Cop supports --auto-correct. Style/MethodCallParentheses: Enabled: false @@ -223,7 +282,7 @@ Style/MethodCallParentheses: Style/MethodMissing: Enabled: false -# Offense count: 85 +# Offense count: 95 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false @@ -240,14 +299,14 @@ Style/NestedParenthesizedCalls: Style/Next: Enabled: false -# Offense count: 8 +# Offense count: 12 # Cop supports --auto-correct. # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. # SupportedOctalStyles: zero_with_o, zero_only Style/NumericLiteralPrefix: Enabled: false -# Offense count: 64 +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: predicate, comparison @@ -259,7 +318,7 @@ Style/NumericPredicate: Style/ParallelAssignment: Enabled: false -# Offense count: 264 +# Offense count: 294 # Cop supports --auto-correct. # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -277,7 +336,7 @@ Style/PercentQLiterals: Style/PerlBackrefs: Enabled: false -# Offense count: 35 +# Offense count: 38 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ @@ -285,7 +344,7 @@ Style/PerlBackrefs: Style/PredicateName: Enabled: false -# Offense count: 27 +# Offense count: 26 # Cop supports --auto-correct. Style/PreferredHashMethods: Enabled: false @@ -317,12 +376,12 @@ Style/RedundantException: Style/RedundantFreeze: Enabled: false -# Offense count: 408 +# Offense count: 427 # Cop supports --auto-correct. Style/RedundantSelf: Enabled: false -# Offense count: 93 +# Offense count: 97 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed @@ -334,7 +393,12 @@ Style/RegexpLiteral: Style/RescueModifier: Enabled: false -# Offense count: 5 +# Offense count: 114 +# Cop supports --auto-correct. +Style/SafeNavigation: + Enabled: false + +# Offense count: 7 # Cop supports --auto-correct. Style/SelfAssignment: Enabled: false @@ -351,7 +415,7 @@ Style/SingleLineBlockParams: Style/SingleLineMethods: Enabled: false -# Offense count: 124 +# Offense count: 125 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: space, no_space @@ -364,19 +428,19 @@ Style/SpaceBeforeBlockBraces: Style/SpaceBeforeFirstArg: Enabled: false -# Offense count: 141 +# Offense count: 145 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space Style/SpaceInsideBlockBraces: Enabled: false -# Offense count: 96 +# Offense count: 99 # Cop supports --auto-correct. Style/SpaceInsideBrackets: Enabled: false -# Offense count: 62 +# Offense count: 65 # Cop supports --auto-correct. Style/SpaceInsideParens: Enabled: false @@ -386,21 +450,21 @@ Style/SpaceInsideParens: Style/SpaceInsidePercentLiteralDelimiters: Enabled: false -# Offense count: 40 +# Offense count: 41 # Cop supports --auto-correct. # Configuration parameters: SupportedStyles. # SupportedStyles: use_perl_names, use_english_names Style/SpecialGlobalVars: EnforcedStyle: use_perl_names -# Offense count: 30 +# Offense count: 31 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: single_quotes, double_quotes Style/StringLiteralsInInterpolation: Enabled: false -# Offense count: 32 +# Offense count: 33 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. # IgnoredMethods: respond_to, define_method @@ -414,7 +478,7 @@ Style/SymbolProc: Style/TernaryParentheses: Enabled: false -# Offense count: 24 +# Offense count: 29 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma diff --git a/.scss-lint.yml b/.scss-lint.yml index 66f9975d4ce..71df6be6a15 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -79,7 +79,7 @@ linters: # HEX colors should use three-character values where possible. HexLength: - enabled: true + enabled: false # HEX color values should use lower-case colors to differentiate between # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. diff --git a/CHANGELOG b/CHANGELOG index 84a6702907f..d72a5ad1ddd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,90 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.13.0 (unreleased) + - Update runner version only when updating contacted_at + - Add link from system note to compare with previous version + - Use gitlab-shell v3.6.2 (GIT TRACE logging) + - Fix centering of custom header logos (Ashley Dumaine) + - AbstractReferenceFilter caches project_refs on RequestStore when active + - Replaced the check sign to arrow in the show build view. !6501 + - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Speed-up group milestones show page + - Keep refs for each deployment + - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) + - Add more tests for calendar contribution (ClemMakesApps) + - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references + - Simplify Mentionable concern instance methods + - Fix permission for setting an issue's due date + - Expose expires_at field when sharing project on API + - Fix VueJS template tags being rendered in code comments + - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) + - Allow the Koding integration to be configured through the API + - Added soft wrap button to repository file/blob editor + - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) + - Fix todos page mobile viewport layout (ClemMakesApps) + - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) + - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) + - Fix that manual jobs would no longer block jobs in the next stage. !6604 + - Add configurable email subject suffix (Fu Xu) + - Use a ConnectionPool for Rails.cache on Sidekiq servers + - Replace `alias_method_chain` with `Module#prepend` + - Enable GitLab Import/Export for non-admin users. + - Preserve label filters when sorting !6136 (Joseph Frazier) + - Only update issuable labels if they have been changed + - Take filters in account in issuable counters. !6496 + - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) + - Prevent flash alert text from being obscured when container is fluid + - Append issue template to existing description !6149 (Joseph Frazier) + - Trending projects now only show public projects and the list of projects is cached for a day + - Revoke button in Applications Settings underlines on hover. + - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) + - Fix Long commit messages overflow viewport in file tree + - Revert avoid touching file system on Build#artifacts? + - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created + - Add broadcast messages and alerts below sub-nav + - Better empty state for Groups view + - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) + - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 + - Add organization field to user profile + - Fix resolved discussion display in side-by-side diff view !6575 + - Optimize GitHub importing for speed and memory + - API: expose pipeline data in builds API (!6502, Guilherme Salazar) + - Notify the Merger about merge after successful build (Dimitris Karakasilis) + - Reduce queries needed to find users using their SSH keys when pushing commits + - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) + - Fix broken repository 500 errors in project list + - Fix Pipeline list commit column width should be adjusted + - Close todos when accepting merge requests via the API !6486 (tonygambone) + - Changed Slack service user referencing from full name to username (Sebastian Poxhofer) + - Add Container Registry on/off status to Admin Area !6638 (the-undefined) + - Grouped pipeline dropdown is a scrollable container + +v 8.12.4 (unreleased) + - Fix type mismatch bug when closing Jira issue + - Skip wiki creation when GitHub project has wiki enabled + - Fix failed project deletion when feature visibility set to private + - Fix issues importing services via Import/Export + - Restrict failed login attempts for users with 2FA enabled + - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell) + +v 8.12.3 + - Update Gitlab Shell to support low IO priority for storage moves + +v 8.12.2 + - Fix Import/Export not recognising correctly the imported services. + - Fix snippets pagination + - Fix "Create project" button layout when visibility options are restricted + - Fix List-Unsubscribe header in emails + - Fix IssuesController#show degradation including project on loaded notes + - Fix an issue with the "Commits" section of the cycle analytics summary. !6513 + - Fix errors importing project feature and milestone models using GitLab project import + - Make JWT messages Docker-compatible + - Fix duplicate branch entry in the merge request version compare dropdown + - Respect the fork_project permission when forking projects + - Only update issuable labels if they have been changed + - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv) + - Fix resolve discussion buttons endpoint path + - Refactor remnants of CoffeeScript destructured opts and super !6261 v 8.12.4 (unreleased) - Set GitLab project exported file permissions to owner only @@ -23,6 +106,7 @@ v 8.12.0 - Allow to set request_access_enabled for groups and projects - Cleanup misalignments in Issue list view !6206 - Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist + - Add Pipelines for Commit - Prune events older than 12 months. (ritave) - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell) - Fix issues/merge-request templates dropdown for forked projects @@ -49,6 +133,7 @@ v 8.12.0 - Fix long comments in diffs messing with table width - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Fix pagination on user snippets page + - Honor "fixed layout" preference in more places !6422 - Run CI builds with the permissions of users !5735 - Fix sorting of issues in API - Fix download artifacts button links !6407 @@ -65,6 +150,7 @@ v 8.12.0 - Reduce contributions calendar data payload (ClemMakesApps) - Show all pipelines for merge requests even from discarded commits !6414 - Replace contributions calendar timezone payload with dates (ClemMakesApps) + - Changed MR widget build status to pipeline status !6335 - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Enable pipeline events by default !6278 - Move parsing of sidekiq ps into helper !6245 (pascalbetz) @@ -88,6 +174,7 @@ v 8.12.0 - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Add textarea autoresize after comment (ClemMakesApps) - Do not write SSH public key 'comments' to authorized_keys !6381 + - Add due date to issue todos - Refresh todos count cache when an Issue/MR is deleted - Fix branches page dropdown sort alignment (ClemMakesApps) - Hides merge request button on branches page is user doesn't have permissions @@ -114,6 +201,7 @@ v 8.12.0 - Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084 - Show queued time when showing a pipeline !6084 - Remove unused mixins (ClemMakesApps) + - Fix issue board label filtering appending already filtered labels - Add search to all issue board lists - Scroll active tab into view on mobile - Fix groups sort dropdown alignment (ClemMakesApps) @@ -198,6 +286,12 @@ v 8.12.0 - Fix non-master branch readme display in tree view - Add UX improvements for merge request version diffs +v 8.11.8 + - Respect the fork_project permission when forking projects + - Set a restrictive CORS policy on the API for credentialed requests + - API: disable rails session auth for non-GET/HEAD requests + - Escape HTML nodes in builds commands in CI linter + v 8.11.7 - Avoid conflict with admin labels when importing GitHub labels. !6158 - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234 @@ -417,6 +511,12 @@ v 8.11.0 - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done +v 8.10.11 + - Respect the fork_project permission when forking projects + - Set a restrictive CORS policy on the API for credentialed requests + - API: disable rails session auth for non-GET/HEAD requests + - Escape HTML nodes in builds commands in CI linter + v 8.10.10 - Allow the Rails cookie to be used for API authentication. @@ -653,6 +753,12 @@ v 8.10.0 - Show tooltip on GitLab export link in new project page - Fix import_data wrongly saved as a result of an invalid import_url !5206 +v 8.9.11 + - Respect the fork_project permission when forking projects + - Set a restrictive CORS policy on the API for credentialed requests + - API: disable rails session auth for non-GET/HEAD requests + - Escape HTML nodes in builds commands in CI linter + v 8.9.10 - Allow the Rails cookie to be used for API authentication. diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 40c341bdcdb..4a788a01dad 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.6.0 +3.6.3 @@ -6,10 +6,8 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with gem 'responders', '~> 2.0' -# Specify a sprockets version due to increased performance -# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069 -gem 'sprockets', '~> 3.6.0' -gem 'sprockets-es6' +gem 'sprockets', '~> 3.7.0' +gem 'sprockets-es6', '~> 0.9.2' # Default values for AR models gem 'default_value_for', '~> 3.0.0' @@ -19,7 +17,7 @@ gem 'mysql2', '~> 0.3.16', group: :mysql gem 'pg', '~> 0.18.2', group: :postgres # Authentication libraries -gem 'devise', '~> 4.0' +gem 'devise', '~> 4.2' gem 'doorkeeper', '~> 4.2.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' @@ -53,7 +51,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.6.6' +gem 'gitlab_git', '~> 10.6.7' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -101,17 +99,17 @@ gem 'unf', '~> 0.1.4' gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing -gem 'html-pipeline', '~> 1.11.0' -gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' -gem 'github-markup', '~> 1.4' -gem 'redcarpet', '~> 3.3.3' -gem 'RedCloth', '~> 4.3.2' -gem 'rdoc', '~>3.6' -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 'html-pipeline', '~> 1.11.0' +gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie' +gem 'github-markup', '~> 1.4' +gem 'redcarpet', '~> 3.3.3' +gem 'RedCloth', '~> 4.3.2' +gem 'rdoc', '~>3.6' +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' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM @@ -122,8 +120,8 @@ gem 'diffy', '~> 3.0.3' # Application server group :unicorn do - gem 'unicorn', '~> 4.9.0' - gem 'unicorn-worker-killer', '~> 0.4.2' + gem 'unicorn', '~> 5.1.0' + gem 'unicorn-worker-killer', '~> 0.4.4' end # State machine @@ -132,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0' gem 'after_commit_queue', '~> 1.3.0' # Issue tags -gem 'acts-as-taggable-on', '~> 3.4' +gem 'acts-as-taggable-on', '~> 4.0' # Background jobs gem 'sidekiq', '~> 4.2' @@ -212,7 +210,7 @@ gem 'oj', '~> 2.17.4' gem 'chronic', '~> 0.10.2' gem 'chronic_duration', '~> 0.10.6' -gem 'sass-rails', '~> 5.0.0' +gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' gem 'turbolinks', '~> 2.5.0' @@ -297,12 +295,11 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.42.0', require: false + gem 'rubocop', '~> 0.43.0', require: false gem 'rubocop-rspec', '~> 1.5.0', 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 - gem 'flog', '~> 4.3.2', require: false gem 'flay', '~> 2.6.1', require: false gem 'bundler-audit', '~> 0.5.0', require: false @@ -330,7 +327,7 @@ gem 'mail_room', '~> 0.8' gem 'email_reply_parser', '~> 0.5.8' -gem 'ruby-prof', '~> 0.15.9' +gem 'ruby-prof', '~> 0.16.2' ## CI gem 'activerecord-session_store', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 66e566de3c1..96b49faf727 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,8 +44,8 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - acts-as-taggable-on (3.5.0) - activerecord (>= 3.2, < 5) + acts-as-taggable-on (4.0.0) + activerecord (>= 4.0) addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) @@ -157,11 +157,15 @@ GEM database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) + deckar01-task_list (1.0.5) + activesupport (~> 4.0) + html-pipeline + rack (~> 1.0) default_value_for (3.0.2) activerecord (>= 3.2.0, < 5.1) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (4.1.1) + devise (4.2.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 5.1) @@ -209,9 +213,6 @@ GEM flay (2.6.1) ruby_parser (~> 3.0) sexp_processor (~> 4.0) - flog (4.3.2) - ruby_parser (~> 3.1, > 3.1.0) - sexp_processor (~> 4.4) flowdock (0.7.1) httparty (~> 0.7) multi_json @@ -279,7 +280,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.6.6) + gitlab_git (10.6.7) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -486,7 +487,7 @@ GEM orm_adapter (0.5.0) paranoia (2.1.4) activerecord (~> 4.0) - parser (2.3.1.2) + parser (2.3.1.4) ast (~> 2.2) pg (0.18.4) pkg-config (1.1.7) @@ -553,7 +554,7 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) - raindrops (0.15.0) + raindrops (0.17.0) rake (10.5.0) rb-fsevent (0.9.6) rb-inotify (0.9.5) @@ -587,7 +588,7 @@ GEM request_store (1.3.1) rerun (0.11.0) listen (~> 3.0) - responders (2.1.1) + responders (2.3.0) railties (>= 4.2.0, < 5.1) rinku (2.0.0) rotp (2.1.2) @@ -619,7 +620,7 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.5.0) - rubocop (0.42.0) + rubocop (0.43.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -629,7 +630,7 @@ GEM rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) - ruby-prof (0.15.9) + ruby-prof (0.16.2) ruby-progressbar (1.8.1) ruby-saml (1.3.0) nokogiri (>= 1.5.10) @@ -644,7 +645,7 @@ GEM sanitize (2.1.0) nokogiri (>= 1.4.4) sass (3.4.22) - sass-rails (5.0.5) + sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -705,10 +706,10 @@ GEM spring (>= 0.9.1) spring-commands-teaspoon (0.0.2) spring (>= 0.9.1) - sprockets (3.6.3) + sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-es6 (0.9.0) + sprockets-es6 (0.9.2) babel-source (>= 5.8.11) babel-transpiler sprockets (>= 3.0.0) @@ -728,8 +729,6 @@ GEM ffi sysexits (1.2.0) systemu (2.6.5) - task_list (1.0.2) - html-pipeline teaspoon (1.1.5) railties (>= 3.2.5, < 6) teaspoon-jasmine (2.2.0) @@ -759,9 +758,8 @@ GEM unf_ext unf_ext (0.0.7.2) unicode-display_width (1.1.1) - unicorn (4.9.0) + unicorn (5.1.0) kgio (~> 2.6) - rack raindrops (~> 0.7) unicorn-worker-killer (0.4.4) get_process_mem (~> 0) @@ -804,7 +802,7 @@ DEPENDENCIES RedCloth (~> 4.3.2) ace-rails-ap (~> 4.1.0) activerecord-session_store (~> 1.0.0) - acts-as-taggable-on (~> 3.4) + acts-as-taggable-on (~> 4.0) addressable (~> 2.3.8) after_commit_queue (~> 1.3.0) akismet (~> 2.0) @@ -835,8 +833,9 @@ DEPENDENCIES creole (~> 0.5.0) d3_rails (~> 3.5.0) database_cleaner (~> 1.5.0) + deckar01-task_list (= 1.0.5) default_value_for (~> 3.0.0) - devise (~> 4.0) + devise (~> 4.2) devise-two-factor (~> 3.0.0) diffy (~> 3.0.3) doorkeeper (~> 4.2.0) @@ -846,7 +845,6 @@ DEPENDENCIES factory_girl_rails (~> 4.6.0) ffaker (~> 2.0.0) flay (~> 2.6.1) - flog (~> 4.3.2) fog-aws (~> 0.9) fog-azure (~> 0.0) fog-core (~> 1.40) @@ -862,7 +860,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.6.6) + gitlab_git (~> 10.6.7) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) @@ -940,12 +938,12 @@ DEPENDENCIES rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) - rubocop (~> 0.42.0) + rubocop (~> 0.43.0) rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) - ruby-prof (~> 0.15.9) + ruby-prof (~> 0.16.2) sanitize (~> 2.0) - sass-rails (~> 5.0.0) + sass-rails (~> 5.0.6) scss_lint (~> 0.47.0) sdoc (~> 0.3.20) seed-fu (~> 2.3.5) @@ -964,11 +962,10 @@ DEPENDENCIES spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) - sprockets (~> 3.6.0) - sprockets-es6 + sprockets (~> 3.7.0) + sprockets-es6 (~> 0.9.2) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) - task_list (~> 1.0.2) teaspoon (~> 1.1.0) teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.4.2) @@ -979,8 +976,8 @@ DEPENDENCIES uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) unf (~> 0.1.4) - unicorn (~> 4.9.0) - unicorn-worker-killer (~> 0.4.2) + unicorn (~> 5.1.0) + unicorn-worker-killer (~> 0.4.4) version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.2) @@ -989,4 +986,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.1 + 1.13.2 diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js deleted file mode 100644 index d4a4c7abaa1..00000000000 --- a/app/assets/javascripts/LabelManager.js +++ /dev/null @@ -1,115 +0,0 @@ -(function() { - this.LabelManager = (function() { - LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time'; - - function LabelManager(opts) { - // Defaults - var ref, ref1, ref2; - if (opts == null) { - opts = {}; - } - this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels'); - this.prioritizedLabels.sortable({ - items: 'li', - placeholder: 'list-placeholder', - axis: 'y', - update: this.onPrioritySortUpdate.bind(this) - }); - this.bindEvents(); - } - - LabelManager.prototype.bindEvents = function() { - return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); - }; - - LabelManager.prototype.onTogglePriorityClick = function(e) { - var $btn, $label, $tooltip, _this, action; - e.preventDefault(); - _this = e.data; - $btn = $(e.currentTarget); - $label = $("#" + ($btn.data('domId'))); - action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; - // Make sure tooltip will hide - $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby'))); - $tooltip.tooltip('destroy'); - return _this.toggleLabelPriority($label, action); - }; - - LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { - var $from, $target, _this, url, xhr; - if (persistState == null) { - persistState = true; - } - _this = this; - url = $label.find('.js-toggle-priority').data('url'); - $target = this.prioritizedLabels; - $from = this.otherLabels; - // Optimistic update - if (action === 'remove') { - $target = this.otherLabels; - $from = this.prioritizedLabels; - } - if ($from.find('li').length === 1) { - $from.find('.empty-message').removeClass('hidden'); - } - if (!$target.find('li').length) { - $target.find('.empty-message').addClass('hidden'); - } - $label.detach().appendTo($target); - // Return if we are not persisting state - if (!persistState) { - return; - } - if (action === 'remove') { - xhr = $.ajax({ - url: url, - type: 'DELETE' - }); - // Restore empty message - if (!$from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - } else { - xhr = this.savePrioritySort($label, action); - } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); - }; - - LabelManager.prototype.onPrioritySortUpdate = function() { - var xhr; - xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); - }; - - LabelManager.prototype.savePrioritySort = function() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } - }); - }; - - LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { - var action; - action = originalAction === 'remove' ? 'add' : 'remove'; - this.toggleLabelPriority($label, action, false); - return new Flash(this.errorMessage, 'alert'); - }; - - LabelManager.prototype.getSortedLabelsIds = function() { - var sortedIds; - sortedIds = []; - this.prioritizedLabels.find('li').each(function() { - return sortedIds.push($(this).data('id')); - }); - return sortedIds; - }; - - return LabelManager; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/LabelManager.js.es6 b/app/assets/javascripts/LabelManager.js.es6 new file mode 100644 index 00000000000..bc68e53504f --- /dev/null +++ b/app/assets/javascripts/LabelManager.js.es6 @@ -0,0 +1,106 @@ +((global) => { + + class LabelManager { + constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { + this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); + this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); + this.otherLabels = otherLabels || $('.js-other-labels'); + this.errorMessage = 'Unable to update label prioritization at this time'; + this.prioritizedLabels.sortable({ + items: 'li', + placeholder: 'list-placeholder', + axis: 'y', + update: this.onPrioritySortUpdate.bind(this) + }); + this.bindEvents(); + } + + bindEvents() { + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + } + + onTogglePriorityClick(e) { + e.preventDefault(); + const _this = e.data; + const $btn = $(e.currentTarget); + const $label = $(`#${$btn.data('domId')}`); + const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); + $tooltip.tooltip('destroy'); + return _this.toggleLabelPriority($label, action); + } + + toggleLabelPriority($label, action, persistState) { + if (persistState == null) { + persistState = true; + } + let xhr; + const _this = this; + const url = $label.find('.js-toggle-priority').data('url'); + let $target = this.prioritizedLabels; + let $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + if ($from.find('li').length === 1) { + $from.find('.empty-message').removeClass('hidden'); + } + if (!$target.find('li').length) { + $target.find('.empty-message').addClass('hidden'); + } + $label.detach().appendTo($target); + // Return if we are not persisting state + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url, + type: 'DELETE' + }); + // Restore empty message + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); + } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + } + + onPrioritySortUpdate() { + const xhr = this.savePrioritySort(); + return xhr.fail(function() { + return new Flash(this.errorMessage, 'alert'); + }); + } + + savePrioritySort() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + } + + rollbackLabelPosition($label, originalAction) { + const action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); + } + + getSortedLabelsIds() { + const sortedIds = []; + this.prioritizedLabels.find('li').each(function() { + sortedIds.push($(this).data('id')); + }); + return sortedIds; + } + } + + gl.LabelManager = LabelManager; + +})(window.gl || (window.gl = {})); + diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 7e5e9fa9ae5..599331df3f5 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -23,12 +23,13 @@ }, // Return groups list. Filtered by query // Only active groups retrieved - groups: function(query, skip_ldap, callback) { + groups: function(query, skip_ldap, skip_groups, callback) { var url = Api.buildUrl(Api.groupsPath); return $.ajax({ url: url, data: { search: query, + skip_groups: skip_groups, per_page: 20 }, dataType: "json" diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c029bf3b5ca..8a61669822c 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -247,7 +247,7 @@ $this.toggleClass('active'); var notesHolders = $this.closest('.diff-file').find('.notes_holder'); if ($this.hasClass('active')) { - notesHolders.show(); + notesHolders.show().find('.hide').show(); } else { notesHolders.hide(); } diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 0decc6d09e6..44af1c135a0 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -357,7 +357,7 @@ $('ul.emoji-menu-search, h5.emoji-search').remove(); if (term) { // Generate a search result block - h5 = $('<h5>').text('Search results'); + h5 = $('<h5 class="emoji-search" />').text('Search results'); found_emojis = _this.searchEmojis(term).show(); ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js deleted file mode 100644 index 68758574967..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js +++ /dev/null @@ -1,46 +0,0 @@ - -/*= require blob/template_selector */ - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.BlobCiYamlSelector = (function(superClass) { - extend(BlobCiYamlSelector, superClass); - - function BlobCiYamlSelector() { - return BlobCiYamlSelector.__super__.constructor.apply(this, arguments); - } - - BlobCiYamlSelector.prototype.requestFile = function(query) { - return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); - }; - - return BlobCiYamlSelector; - - })(TemplateSelector); - - this.BlobCiYamlSelectors = (function() { - function BlobCiYamlSelectors(opts) { - var ref; - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor; - this.$dropdowns.each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new BlobCiYamlSelector({ - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown, - editor: _this.editor - }); - }; - })(this)); - } - - return BlobCiYamlSelectors; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 new file mode 100644 index 00000000000..d6ea4f84f57 --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -0,0 +1,40 @@ +/*= require blob/template_selector */ +((global) => { + + class BlobCiYamlSelector extends gl.TemplateSelector { + requestFile(query) { + return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); + } + + requestFileSuccess(file) { + return super.requestFileSuccess(file); + } + } + + global.BlobCiYamlSelector = BlobCiYamlSelector; + + class BlobCiYamlSelectors { + constructor({ editor, $dropdowns } = {}) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobCiYamlSelector({ + editor, + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown + }); + }); + } + } + + global.BlobCiYamlSelectors = BlobCiYamlSelectors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 54a09e919f8..cd746b05cf6 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -18,6 +18,6 @@ return BlobGitignoreSelector; - })(TemplateSelector); + })(gl.TemplateSelector); }).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 9a8ef08f4e5..2701df3e6de 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -23,6 +23,6 @@ return BlobLicenseSelector; - })(TemplateSelector); + })(gl.TemplateSelector); }).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js deleted file mode 100644 index 39237705e8d..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js +++ /dev/null @@ -1,25 +0,0 @@ -(function() { - this.BlobLicenseSelectors = (function() { - function BlobLicenseSelectors(opts) { - var ref; - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor; - this.$dropdowns.each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new BlobLicenseSelector({ - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - editor: _this.editor - }); - }; - })(this)); - } - - return BlobLicenseSelectors; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 new file mode 100644 index 00000000000..153ed457559 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -0,0 +1,21 @@ +((global) => { + class BlobLicenseSelectors { + constructor({ $dropdowns, editor }) { + this.$dropdowns = $('.js-license-selector'); + this.editor = editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobLicenseSelector({ + editor, + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + }); + }); + } + } + + global.BlobLicenseSelectors = BlobLicenseSelectors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js deleted file mode 100644 index 95352164d76..00000000000 --- a/app/assets/javascripts/blob/template_selector.js +++ /dev/null @@ -1,100 +0,0 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.TemplateSelector = (function() { - function TemplateSelector(opts) { - var ref; - if (opts == null) { - opts = {}; - } - this.onClick = bind(this.onClick, this); - this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name'); - this.dropdownIcon = $('.fa-chevron-down', this.dropdown); - this.buildDropdown(); - this.bindEvents(); - this.onFilenameUpdate(); - - this.autosizeUpdateEvent = document.createEvent('Event'); - this.autosizeUpdateEvent.initEvent('autosize:update', true, false); - } - - TemplateSelector.prototype.buildDropdown = function() { - return this.dropdown.glDropdown({ - data: this.data, - filterable: true, - selectable: true, - toggleLabel: this.toggleLabel, - search: { - fields: ['name'] - }, - clicked: this.onClick, - text: function(item) { - return item.name; - } - }); - }; - - TemplateSelector.prototype.bindEvents = function() { - return this.$input.on('keyup blur', (function(_this) { - return function(e) { - return _this.onFilenameUpdate(); - }; - })(this)); - }; - - TemplateSelector.prototype.toggleLabel = function(item) { - return item.name; - }; - - TemplateSelector.prototype.onFilenameUpdate = function() { - var filenameMatches; - if (!this.$input.length) { - return; - } - filenameMatches = this.pattern.test(this.$input.val().trim()); - if (!filenameMatches) { - this.wrapper.addClass('hidden'); - return; - } - return this.wrapper.removeClass('hidden'); - }; - - TemplateSelector.prototype.onClick = function(item, el, e) { - e.preventDefault(); - return this.requestFile(item); - }; - - TemplateSelector.prototype.requestFile = function(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. - }; - - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { - this.editor.setValue(file.content, 1); - if (!skipFocus) this.editor.focus(); - - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - }; - - TemplateSelector.prototype.startLoadingSpinner = function() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - }; - - TemplateSelector.prototype.stopLoadingSpinner = function() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); - }; - - return TemplateSelector; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 new file mode 100644 index 00000000000..4e309e480b0 --- /dev/null +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -0,0 +1,102 @@ +((global) => { + class TemplateSelector { + constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { + this.onClick = this.onClick.bind(this); + this.dropdown = dropdown; + this.data = data; + this.pattern = pattern; + this.wrapper = wrapper; + this.editor = editor; + this.fileEndpoint = fileEndpoint; + this.$input = $input || $('#file_name'); + this.dropdownIcon = $('.fa-chevron-down', this.dropdown); + this.buildDropdown(); + this.bindEvents(); + this.onFilenameUpdate(); + + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); + } + + buildDropdown() { + return this.dropdown.glDropdown({ + data: this.data, + filterable: true, + selectable: true, + toggleLabel: this.toggleLabel, + search: { + fields: ['name'] + }, + clicked: this.onClick, + text: function(item) { + return item.name; + } + }); + } + + bindEvents() { + return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); + } + + toggleLabel(item) { + return item.name; + } + + onFilenameUpdate() { + var filenameMatches; + if (!this.$input.length) { + return; + } + filenameMatches = this.pattern.test(this.$input.val().trim()); + if (!filenameMatches) { + this.wrapper.addClass('hidden'); + return; + } + return this.wrapper.removeClass('hidden'); + } + + onClick(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } + + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } + + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) + requestFileSuccess(file, { skipFocus, append } = {}) { + const oldValue = this.editor.getValue(); + let newValue = file.content; + + if (append && oldValue.length && oldValue !== newValue) { + newValue = oldValue + '\n\n' + newValue; + } + + this.editor.setValue(newValue, 1); + if (!skipFocus) this.editor.focus(); + + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); + } + } + + startLoadingSpinner() { + this.dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + stopLoadingSpinner() { + this.dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } + } + + global.TemplateSelector = TemplateSelector; + })(window.gl || ( window.gl = {})); + diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index b846bab0424..8db4f6a3b28 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -22,13 +22,14 @@ // submitted textarea })(this)); this.initModePanesAndLinks(); - new BlobLicenseSelectors({ + this.initSoftWrap(); + new gl.BlobLicenseSelectors({ editor: this.editor }); new BlobGitignoreSelectors({ editor: this.editor }); - new BlobCiYamlSelectors({ + new gl.BlobCiYamlSelectors({ editor: this.editor }); } @@ -50,6 +51,7 @@ this.$editModePanes.hide(); currentPane.fadeIn(200); if (paneId === "#preview") { + this.$toggleButton.hide(); return $.post(currentLink.data("preview-url"), { content: this.editor.getValue() }, function(response) { @@ -57,10 +59,23 @@ return currentPane.syntaxHighlight(); }); } else { + this.$toggleButton.show(); return this.editor.focus(); } }; + EditBlob.prototype.initSoftWrap = function() { + this.isSoftWrapped = false; + this.$toggleButton = $('.soft-wrap-toggle'); + this.$toggleButton.on('click', this.toggleSoftWrap.bind(this)); + }; + + EditBlob.prototype.toggleSoftWrap = function(e) { + this.isSoftWrapped = !this.isSoftWrapped; + this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); + this.editor.getSession().setUseWrapMode(this.isSoftWrapped); + }; + return EditBlob; })(); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 78d21c0552a..f336bfc36d6 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -146,7 +146,7 @@ $date = $('.js-artifacts-remove'); if ($date.length) { date = $date.text(); - return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); + return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); } }; diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 4e3a28cd163..294d2c9052c 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -23,8 +23,9 @@ selectable: true, filterable: true, filterByText: true, - fieldName: $dropdown.attr('name'), - filterInput: 'input[type="text"]', + toggleLabel: true, + fieldName: $dropdown.data('field-name'), + filterInput: 'input[type="search"]', renderRow: function(ref) { var link; if (ref.header != null) { diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 3e20db7e308..e23bda2fa4e 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -26,15 +26,15 @@ }; showTooltip = function(target, title) { - return $(target).tooltip({ - container: 'body', - html: 'true', - placement: 'auto bottom', - title: title, - trigger: 'manual' - }).tooltip('show').one('mouseleave', function() { - return $(this).tooltip('hide'); - }); + var $target = $(target); + var originalTitle = $target.data('original-title'); + + $target + .attr('title', 'Copied!') + .tooltip('fixTitle') + .tooltip('show') + .attr('title', originalTitle) + .tooltip('fixTitle'); }; $(function() { diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index c8634b78f2b..8086c10ad6b 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -7,6 +7,9 @@ function Diff() { $('.files .diff-file').singleFileDiff(); this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + if (this.diffViewType() === 'parallel') { + $('.content-wrapper .container-fluid').removeClass('container-limited'); + } $(document).off('click', '.js-unfold'); $(document).on('click', '.js-unfold', (function(_this) { return function(event) { @@ -52,6 +55,10 @@ })(this)); } + Diff.prototype.diffViewType = function() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + Diff.prototype.lineNumbers = function(line) { if (!line.children().length) { return [0, 0]; 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 be6ebc77947..cdedfd1af15 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,13 +1,9 @@ ((w) => { w.ResolveBtn = Vue.extend({ - mixins: [ - ButtonMixins - ], props: { noteId: Number, discussionId: String, resolved: Boolean, - namespacePath: String, projectPath: String, canResolve: Boolean, resolvedBy: String @@ -69,10 +65,10 @@ if (this.isResolved) { promise = ResolveService - .unresolve(this.namespace, this.noteId); + .unresolve(this.projectPath, this.noteId); } else { promise = ResolveService - .resolve(this.namespace, this.noteId); + .resolve(this.projectPath, this.noteId); } promise.then((response) => { 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 e373b06b1eb..0a617034502 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,12 +1,8 @@ ((w) => { w.ResolveDiscussionBtn = Vue.extend({ - mixins: [ - ButtonMixins - ], props: { discussionId: String, mergeRequestId: Number, - namespacePath: String, projectPath: String, canResolve: Boolean, }, @@ -50,7 +46,7 @@ }, methods: { resolve: function () { - ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId); + ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId); } }, created: function () { diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 deleted file mode 100644 index d278678085b..00000000000 --- a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 +++ /dev/null @@ -1,9 +0,0 @@ -((w) => { - w.ButtonMixins = { - computed: { - namespace: function () { - return `${this.namespacePath}/${this.projectPath}`; - } - } - }; -})(window); diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index de771ff814b..2a55f739b31 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -9,32 +9,32 @@ Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); } - prepareRequest(namespace) { + prepareRequest(root) { this.setCSRF(); - Vue.http.options.root = `/${namespace}`; + Vue.http.options.root = root; } - resolve(namespace, noteId) { - this.prepareRequest(namespace); + resolve(projectPath, noteId) { + this.prepareRequest(projectPath); return this.noteResource.save({ noteId }, {}); } - unresolve(namespace, noteId) { - this.prepareRequest(namespace); + unresolve(projectPath, noteId) { + this.prepareRequest(projectPath); return this.noteResource.delete({ noteId }, {}); } - toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) { + toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) { const discussion = CommentsStore.state[discussionId], isResolved = discussion.isResolved(); let promise; if (isResolved) { - promise = this.unResolveAll(namespace, mergeRequestId, discussionId); + promise = this.unResolveAll(projectPath, mergeRequestId, discussionId); } else { - promise = this.resolveAll(namespace, mergeRequestId, discussionId); + promise = this.resolveAll(projectPath, mergeRequestId, discussionId); } promise.then((response) => { @@ -57,10 +57,10 @@ }) } - resolveAll(namespace, mergeRequestId, discussionId) { + resolveAll(projectPath, mergeRequestId, discussionId) { const discussion = CommentsStore.state[discussionId]; - this.prepareRequest(namespace); + this.prepareRequest(projectPath); discussion.loading = true; @@ -70,10 +70,10 @@ }, {}); } - unResolveAll(namespace, mergeRequestId, discussionId) { + unResolveAll(projectPath, mergeRequestId, discussionId) { const discussion = CommentsStore.state[discussionId]; - this.prepareRequest(namespace); + this.prepareRequest(projectPath); discussion.loading = true; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ddf11ecf34c..8d99b12102d 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -26,7 +26,7 @@ case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); - new IssuableBulkActions(); + new gl.IssuableBulkActions(); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': @@ -40,7 +40,7 @@ new Milestone(); break; case 'dashboard:todos:index': - new Todos(); + new gl.Todos(); break; case 'projects:milestones:new': case 'projects:milestones:edit': @@ -59,7 +59,9 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.issue-form')); new IssuableForm($('.issue-form')); - new IssuableTemplateSelectors(); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:new': case 'projects:merge_requests:edit': @@ -67,7 +69,9 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); - new IssuableTemplateSelectors(); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); break; case 'projects:tags:new': new ZenMode(); @@ -165,7 +169,7 @@ break; case 'projects:labels:index': if ($('.prioritized-labels').length) { - new LabelManager(); + new gl.LabelManager(); } break; case 'projects:network:show': @@ -279,7 +283,7 @@ Dispatcher.prototype.initSearch = function() { // Only when search form is present if ($('.search').length) { - return new SearchAutocomplete(); + return new gl.SearchAutocomplete(); } }; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1b6db641200..d4403375643 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -443,6 +443,7 @@ var contentHtml; this.resetRows(); this.addArrowKeyEvent(); + if (this.options.setIndeterminateIds) { this.options.setIndeterminateIds.call(this); } @@ -460,9 +461,21 @@ if (this.options.filterable) { this.filterInput.focus(); } + + if (this.options.showMenuAbove) { + this.positionMenuAbove(); + } + return this.dropdown.trigger('shown.gl.dropdown'); }; + GitLabDropdown.prototype.positionMenuAbove = function() { + var $button = $(this.el); + var $menu = this.dropdown.find('.dropdown-menu'); + + $menu.css('top', ($button.height() + $menu.height()) * -1); + }; + GitLabDropdown.prototype.hidden = function(e) { var $input; this.resetRows(); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 7c2eebcdd44..5f06186504b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -5,14 +5,15 @@ function GroupsSelect() { $('.ajax-groups-select').each((function(_this) { return function(i, select) { - var skip_ldap; + var skip_ldap, skip_groups; skip_ldap = $(select).hasClass('skip_ldap'); + skip_groups = $(select).data('skip-groups') || []; return $(select).select2({ placeholder: "Search for a group", multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - return Api.groups(query.term, skip_ldap, function(groups) { + return Api.groups(query.term, skip_ldap, skip_groups, function(groups) { var data; data = { results: groups diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 73e2664e9c0..57f7e4ef230 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -51,7 +51,6 @@ }).remove(); // Submit the form to get new data Issuable.filterResults($('.filter-form')); - return $('.js-label-select').trigger('update.label'); }); }, filterResults: (function(_this) { diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js.es6 index 62a7fc9a06c..0808f538f01 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js +++ b/app/assets/javascripts/issues-bulk-assignment.js.es6 @@ -1,13 +1,10 @@ -(function() { - this.IssuableBulkActions = (function() { - function IssuableBulkActions(opts) { - // Set defaults - var ref, ref1, ref2; - if (opts == null) { - opts = {}; - } - this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li'); - // Save instance +((global) => { + + class IssuableBulkActions { + constructor({ container, form, issues } = {}) { + this.container = container || $('.content'), + this.form = form || this.getElement('.bulk-update'); + this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; this.bindEvents(); @@ -15,53 +12,46 @@ Issuable.initChecks(); } - IssuableBulkActions.prototype.getElement = function(selector) { + getElement(selector) { return this.container.find(selector); - }; + } - IssuableBulkActions.prototype.bindEvents = function() { + bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); - }; + } - IssuableBulkActions.prototype.onFormSubmit = function(e) { + onFormSubmit(e) { e.preventDefault(); return this.submit(); - }; + } - IssuableBulkActions.prototype.submit = function() { - var _this, xhr; - _this = this; - xhr = $.ajax({ + submit() { + const _this = this; + const xhr = $.ajax({ url: this.form.attr('action'), method: this.form.attr('method'), dataType: 'JSON', data: this.getFormDataAsObject() }); - xhr.done(function(response, status, xhr) { - return location.reload(); - }); - xhr.fail(function() { - return new Flash("Issue update failed"); - }); + xhr.done(() => window.location.reload()); + xhr.fail(() => new Flash("Issue update failed")); return xhr.always(this.onFormSubmitAlways.bind(this)); - }; + } - IssuableBulkActions.prototype.onFormSubmitAlways = function() { + onFormSubmitAlways() { return this.form.find('[type="submit"]').enable(); - }; + } - IssuableBulkActions.prototype.getSelectedIssues = function() { + getSelectedIssues() { return this.issues.has('.selected_issue:checked'); - }; + } - IssuableBulkActions.prototype.getLabelsFromSelection = function() { - var labels; - labels = []; + getLabelsFromSelection() { + const labels = []; this.getSelectedIssues().map(function() { - var _labels; - _labels = $(this).data('labels'); - if (_labels) { - return _labels.map(function(labelId) { + const labelsData = $(this).data('labels'); + if (labelsData) { + return labelsData.map(function(labelId) { if (labels.indexOf(labelId) === -1) { return labels.push(labelId); } @@ -69,7 +59,7 @@ } }); return labels; - }; + } /** @@ -77,25 +67,21 @@ * @return {Array} Label IDs */ - IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { - var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; - result = []; - labelsToKeep = []; - ref = this.getElement('.labels-filter .is-indeterminate'); - for (i = 0, len = ref.length; i < len; i++) { - el = ref[i]; - labelsToKeep.push($(el).data('labelId')); - } - ref1 = this.getLabelsFromSelection(); - for (j = 0, len1 = ref1.length; j < len1; j++) { - id = ref1[j]; - // Only the ones that we are not going to keep + getUnmarkedIndeterminedLabels() { + const result = []; + const labelsToKeep = []; + + this.getElement('.labels-filter .is-indeterminate') + .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + + this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { result.push(id); } - } + }); + return result; - }; + } /** @@ -103,9 +89,8 @@ * Returns key/value pairs from form data */ - IssuableBulkActions.prototype.getFormDataAsObject = function() { - var formData; - formData = { + getFormDataAsObject() { + const formData = { update: { state_event: this.form.find('input[name="update[state_event]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), @@ -125,19 +110,18 @@ }); } return formData; - }; + } - IssuableBulkActions.prototype.getLabelsToApply = function() { - var $labels, labelIds; - labelIds = []; - $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); + getLabelsToApply() { + 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())); } }); return labelIds; - }; + } /** @@ -145,11 +129,10 @@ * @return {Array} Array of labels IDs */ - IssuableBulkActions.prototype.getLabelsToRemove = function() { - var indeterminatedLabels, labelsToApply, result; - result = []; - indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - labelsToApply = this.getLabelsToApply(); + 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 @@ -158,10 +141,9 @@ } }); return result; - }; - - return IssuableBulkActions; + } + } - })(); + global.IssuableBulkActions = IssuableBulkActions; -}).call(this); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 51a84066185..66cc3f99b0f 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,8 +4,9 @@ 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; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove; $dropdown = $(dropdown); + $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); labelUrl = $dropdown.data('labels'); @@ -16,6 +17,7 @@ } showNo = $dropdown.data('show-no'); showAny = $dropdown.data('show-any'); + showMenuAbove = $dropdown.data('showMenuAbove'); defaultLabel = $dropdown.data('default-label'); abilityName = $dropdown.data('ability-name'); $selectbox = $dropdown.closest('.selectbox'); @@ -25,6 +27,14 @@ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); + fieldName = $dropdown.data('field-name'); + useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); + propertyName = useId ? 'id' : 'title'; + initialSelected = $selectbox + .find('input[name="' + $dropdown.data('field-name') + '"]') + .map(function () { + return this.value; + }).get(); if (issueUpdateURL != null) { issueURLSplit = issueUpdateURL.split('/'); } @@ -41,9 +51,13 @@ saveLabelData = function() { var data, selected; - selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() { + selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { return this.value; }).get(); + + if (_.isEqual(initialSelected, selected)) return; + initialSelected = selected; + data = {}; data[abilityName] = {}; data[abilityName].label_ids = selected; @@ -67,7 +81,8 @@ if (data.labels.length) { template = labelHTMLTemplate(data); labelCount = data.labels.length; - } else { + } + else { template = labelNoneHTMLTemplate; } $value.removeAttr('style').html(template); @@ -84,7 +99,8 @@ } labelTooltipTitle = labelTitles.join(', '); - } else { + } + else { labelTooltipTitle = ''; $sidebarLabelTooltip.tooltip('destroy'); } @@ -106,6 +122,7 @@ }); }; return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ url: labelUrl @@ -125,23 +142,29 @@ }; }).value(); if ($dropdown.hasClass('js-extra-options')) { + var extraData = []; if (showNo) { - data.unshift({ + extraData.unshift({ id: 0, title: 'No Label' }); } if (showAny) { - data.unshift({ + extraData.unshift({ isAny: true, title: 'Any Label' }); } - if (data.length > 2) { - data.splice(2, 0, 'divider'); + if (extraData.length) { + extraData.push('divider'); + data = extraData.concat(data); } } - return callback(data); + + callback(data); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } }); }, renderRow: function(label, instance) { @@ -149,7 +172,7 @@ $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; - removesAll = label.id === 0 || (label.id == null); + removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { indeterminate = instance.indeterminateIds; active = instance.activeIds; @@ -186,14 +209,16 @@ return color + " " + percentFirst + "%," + color + " " + percentSecond + "% "; }).join(','); color = "linear-gradient(" + color + ")"; - } else { + } + else { if (label.color != null) { color = label.color[0]; } } if (color) { colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; - } else { + } + else { colorEl = ''; } // We need to identify which items are actually labels @@ -211,30 +236,46 @@ }, selectable: true, filterable: true, + selected: $dropdown.data('selected') || [], toggleLabel: function(selected, el) { - var selected_labels; - selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active'); - if (selected && (selected.title != null)) { - if (selected_labels.length > 1) { - return selected.title + " +" + (selected_labels.length - 1) + " more"; - } else { - return selected.title; - } - } else if (!selected && selected_labels.length !== 0) { - if (selected_labels.length > 1) { - return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more"; - } else if (selected_labels.length === 1) { - return $(selected_labels).text(); - } - } else { + var isSelected = el !== null ? el.hasClass('is-active') : false; + var title = selected.title; + var selectedLabels = this.selected; + + if (selected.id === 0) { + this.selected = []; + return 'No Label'; + } + else if (isSelected) { + this.selected.push(title); + } + else { + var index = this.selected.indexOf(title); + this.selected.splice(index, 1); + } + + if (selectedLabels.length === 1) { + return selectedLabels; + } + else if (selectedLabels.length) { + return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; + } + else { return defaultLabel; } }, fieldName: $dropdown.data('field-name'), id: function(label) { + if (label.id <= 0) return; + + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return label.id; + } + if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { return label.title; - } else { + } + else { return label.id; } }, @@ -246,6 +287,11 @@ $selectbox.hide(); // display:block overrides the hide-collapse rule $value.removeAttr('style'); + + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return; + } + if (page === 'projects:boards:show') { return; } @@ -253,9 +299,11 @@ if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { + } + else if ($dropdown.hasClass('js-filter-submit')) { $dropdown.closest('form').submit(); - } else { + } + else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); } @@ -272,21 +320,31 @@ clicked: function(label, $el, e) { var isIssueIndex, isMRIndex, page; _this.enableBulkLabelDropdown(); - if ($dropdown.hasClass('js-filter-bulk-update')) { + + if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { + $dropdown.parent() + .find('.dropdown-clear-active') + .removeClass('is-active') + } + + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { return; } + page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; if (page === 'projects:boards:show') { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; - } else if (label.title) { + } + else if ($el.hasClass('is-active')) { gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title); - } else { + } + else { var filters = gl.issueBoards.BoardsStore.state.filters['label_name']; - filters = filters.filter(function (label) { - return label !== $el.text().trim(); + filters = filters.filter(function (filteredLabel) { + return filteredLabel !== label.title; }); gl.issueBoards.BoardsStore.state.filters['label_name'] = filters; } @@ -294,17 +352,21 @@ gl.issueBoards.BoardsStore.updateFiltersUrl(); e.preventDefault(); return; - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + } + else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (!$dropdown.hasClass('js-multiselect')) { selectedLabel = label.title; return Issuable.filterResults($dropdown.closest('form')); } - } else if ($dropdown.hasClass('js-filter-submit')) { + } + else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); - } else { + } + else { if ($dropdown.hasClass('js-multiselect')) { - } else { + } + else { return saveLabelData(); } } diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index f84a20cf0fe..b8d52becb3f 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -19,7 +19,7 @@ while (i < sURLVariables.length) { sParameterName = sURLVariables[i].split('='); if (sParameterName[0] === sParam) { - values.push(sParameterName[1]); + values.push(sParameterName[1].replace(/\+/g, ' ')); } i++; } diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 index cd92df8ddc5..13ee794ba38 100644 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6 @@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; class MergeConflictDataProvider { getInitialData() { + // TODO: remove reliance on jQuery and DOM state introspection const diffViewType = $.cookie('diff_view'); + const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited'); return { isLoading : true, hasError : false, isParallel : diffViewType === 'parallel', diffViewType : diffViewType, + fixedLayout : fixedLayout, isSubmitting : false, conflictsData : {}, resolutionData : {} @@ -192,14 +195,17 @@ class MergeConflictDataProvider { updateViewType(newType) { const vi = this.vueInstance; - if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { + if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) { return; } - vi.diffView = newType; - vi.isParallel = newType === 'parallel'; - $.cookie('diff_view', newType); // TODO: Make sure that cookie path added. - $('.content-wrapper .container-fluid').toggleClass('container-limited'); + vi.diffViewType = newType; + vi.isParallel = newType === 'parallel'; + $.cookie('diff_view', newType, { + path: (gon && gon.relative_url_root) || '/' + }); + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', !vi.isParallel && vi.fixedLayout); } diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6 index b56fd5aa658..7e756433bf5 100644 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ b/app/assets/javascripts/merge_conflict_resolver.js.es6 @@ -60,9 +60,8 @@ class MergeConflictResolver { $('#conflicts .js-syntax-highlight').syntaxHighlight(); }); - if (this.vue.diffViewType === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout); }) } diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 05644b3d03c..02ff5a382e2 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -36,13 +36,10 @@ }; MergeRequest.prototype.initTabs = function() { - if (this.opts.action !== 'new') { - // `MergeRequests#new` has no tab-persisting or lazy-loading behavior - window.mrTabs = new MergeRequestTabs(this.opts); - } else { - // Show the first tab (Commits) - return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); + if (window.mrTabs) { + window.mrTabs.unbindEvents(); } + window.mrTabs = new MergeRequestTabs(this.opts); }; MergeRequest.prototype.showAllCommits = function() { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 18bbfa7a459..bec11a198a1 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -56,6 +56,8 @@ MergeRequestTabs.prototype.commitsLoaded = false; + MergeRequestTabs.prototype.fixedLayoutPref = null; + function MergeRequestTabs(opts) { this.opts = opts != null ? opts : {}; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; @@ -70,7 +72,12 @@ MergeRequestTabs.prototype.bindEvents = function() { $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - return $(document).on('click', '.js-show-tab', this.showTab); + $(document).on('click', '.js-show-tab', this.showTab); + }; + + MergeRequestTabs.prototype.unbindEvents = function() { + $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); + $(document).off('click', '.js-show-tab', this.showTab); }; MergeRequestTabs.prototype.showTab = function(event) { @@ -85,11 +92,15 @@ if (action === 'commits') { this.loadCommits($target.attr('href')); this.expandView(); + this.resetViewContainer(); } else if (action === 'diffs') { this.loadDiff($target.attr('href')); if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { this.shrinkView(); } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } navBarHeight = $('.navbar-gitlab').outerHeight(); $.scrollTo(".merge-request-details .merge-request-tabs", { offset: -navBarHeight @@ -97,11 +108,14 @@ } else if (action === 'builds') { this.loadBuilds($target.attr('href')); this.expandView(); + this.resetViewContainer(); } else if (action === 'pipelines') { this.loadPipelines($target.attr('href')); this.expandView(); + this.resetViewContainer(); } else { this.expandView(); + this.resetViewContainer(); } if (this.opts.setUrl) { this.setCurrentAction(action); @@ -126,7 +140,7 @@ if (action === 'show') { action = 'notes'; } - return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab'); }; // Replaces the current Merge Request-specific action in the URL with a new one @@ -209,7 +223,7 @@ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); $('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel') { + if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') { _this.expandViewContainer(); } _this.diffsLoaded = true; @@ -308,11 +322,21 @@ MergeRequestTabs.prototype.diffViewType = function() { return $('.inline-parallel-buttons a.active').data('view-type'); - // Returns diff view type }; MergeRequestTabs.prototype.expandViewContainer = function() { - return $('.container-fluid').removeClass('container-limited'); + var $wrapper = $('.content-wrapper .container-fluid'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); + } + $wrapper.removeClass('container-limited'); + }; + + MergeRequestTabs.prototype.resetViewContainer = function() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); + } }; MergeRequestTabs.prototype.shrinkView = function() { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index c8031174dd2..26cc6eb0e96 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -7,7 +7,7 @@ this.currentProject = JSON.parse(currentProject); } $('.js-milestone-select').each(function(i, dropdown) { - var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId; + var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); milestonesUrl = $dropdown.data('milestones'); @@ -15,6 +15,7 @@ selectedMilestone = $dropdown.data('selected'); showNo = $dropdown.data('show-no'); showAny = $dropdown.data('show-any'); + showMenuAbove = $dropdown.data('showMenuAbove'); showUpcoming = $dropdown.data('show-upcoming'); useId = $dropdown.data('use-id'); defaultLabel = $dropdown.data('default-label'); @@ -31,12 +32,12 @@ collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); } return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ url: milestonesUrl }).done(function(data) { - var extraOptions; - extraOptions = []; + var extraOptions = []; if (showAny) { extraOptions.push({ id: 0, @@ -58,10 +59,14 @@ title: 'Upcoming' }); } - if (extraOptions.length > 2) { + if (extraOptions.length) { extraOptions.push('divider'); } - return callback(extraOptions.concat(data)); + + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } }); }, filterable: true, @@ -69,19 +74,20 @@ fields: ['title'] }, selectable: true, - toggleLabel: function(selected) { - if (selected && 'id' in selected) { + toggleLabel: function(selected, el, e) { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { return selected.title; } else { return defaultLabel; } }, + defaultLabel: defaultLabel, fieldName: $dropdown.data('field-name'), text: function(milestone) { return _.escape(milestone.title); }, id: function(milestone) { - if (!useId) { + if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { return milestone.name; } else { return milestone.id; @@ -100,7 +106,8 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update')) { + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); return; } if (page === 'projects:boards:show') { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index c6854f703fb..866a04d3e21 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -432,14 +432,12 @@ var $form = $(xhr.target); if ($form.attr('data-resolve-all') != null) { - var namespacePath = $form.attr('data-namespace-path'), - projectPath = $form.attr('data-project-path') - discussionId = $form.attr('data-discussion-id'), - mergeRequestId = $form.attr('data-noteable-iid'), - namespace = namespacePath + '/' + projectPath; + var projectPath = $form.data('project-path') + discussionId = $form.data('discussion-id'), + mergeRequestId = $form.data('noteable-iid'); if (ResolveService != null) { - ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId); + ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId); } } @@ -854,7 +852,6 @@ .closest('form') .attr('data-discussion-id', discussionId) .attr('data-resolve-all', 'true') - .attr('data-namespace-path', $this.attr('data-namespace-path')) .attr('data-project-path', $this.attr('data-project-path')); }; diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js.es6 index 30cd6f6e470..a1b0126e857 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,47 +1,45 @@ -(function() { - var GitLabCrop, - bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; +((global) => { - GitLabCrop = (function() { - var FILENAMEREGEX; + // Matches everything but the file name + const FILENAMEREGEX = /^.*[\\\/]/; - // Matches everything but the file name - FILENAMEREGEX = /^.*[\\\/]/; + class GitLabCrop { + constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, + exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { - function GitLabCrop(input, opts) { - var ref, ref1, ref2, ref3, ref4; - if (opts == null) { - opts = {}; - } - this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this); - this.onModalHide = bind(this.onModalHide, this); - this.onModalShow = bind(this.onModalShow, this); - this.onPickImageClick = bind(this.onPickImageClick, this); + this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); + this.onModalHide = this.onModalHide.bind(this); + this.onModalShow = this.onModalShow.bind(this); + this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); - // We should rename to avoid spec to fail - // Form will submit the proper input filed with a file using FormData - this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger"); - // Set defaults - this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg; - // Required params - // Ensure needed elements are jquery objects - // If selector is provided we will convert them to a jQuery Object - this.filename = this.getElement(this.filename); - this.previewImage = this.getElement(this.previewImage); - this.pickImageEl = this.getElement(this.pickImageEl); - // Modal elements usually are outside the @form element - this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop; - this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; + this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`); + this.exportWidth = exportWidth; + this.exportHeight = exportHeight; + this.cropBoxWidth = cropBoxWidth; + this.cropBoxHeight = cropBoxHeight; + this.form = this.fileInput.parents('form'); + this.filename = filename; + this.previewImage = previewImage; + this.modalCrop = modalCrop; + this.pickImageEl = pickImageEl; + this.uploadImageBtn = uploadImageBtn; + this.modalCropImg = modalCropImg; + this.filename = this.getElement(filename); + this.previewImage = this.getElement(previewImage); + this.pickImageEl = this.getElement(pickImageEl); + this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop; + this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; + this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg; this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.bindEvents(); } - GitLabCrop.prototype.getElement = function(selector) { + getElement(selector) { return $(selector, this.form); - }; + } - GitLabCrop.prototype.bindEvents = function() { + bindEvents() { var _this; _this = this; this.fileInput.on('change', function(e) { @@ -57,13 +55,13 @@ return _this.onActionBtnClick(btn); }); return this.croppedImageBlob = null; - }; + } - GitLabCrop.prototype.onPickImageClick = function() { + onPickImageClick() { return this.fileInput.trigger('click'); - }; + } - GitLabCrop.prototype.onModalShow = function() { + onModalShow() { var _this; _this = this; return this.modalCropImg.cropper({ @@ -95,44 +93,44 @@ }); } }); - }; + } - GitLabCrop.prototype.onModalHide = function() { + onModalHide() { return this.modalCropImg.attr('src', '').cropper('destroy'); - }; + } - GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image - e.preventDefault(); // Destroy cropper instance + onUploadImageBtnClick(e) { + e.preventDefault(); this.setBlob(); this.setPreview(); this.modalCrop.modal('hide'); return this.fileInput.val(''); - }; + } - GitLabCrop.prototype.onActionBtnClick = function(btn) { + onActionBtnClick(btn) { var data, result; data = $(btn).data(); if (this.modalCropImg.data('cropper') && data.method) { return result = this.modalCropImg.cropper(data.method, data.option); } - }; + } - GitLabCrop.prototype.onFileInputChange = function(e, input) { + onFileInputChange(e, input) { return this.readFile(input); - }; + } - GitLabCrop.prototype.readFile = function(input) { + readFile(input) { var _this, reader; _this = this; reader = new FileReader; - reader.onload = function() { + reader.onload = () => { _this.modalCropImg.attr('src', reader.result); return _this.modalCrop.modal('show'); }; return reader.readAsDataURL(input.files[0]); - }; + } - GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { + dataURLtoBlob(dataURL) { var array, binary, i, k, len, v; binary = atob(dataURL.split(',')[1]); array = []; @@ -143,35 +141,32 @@ return new Blob([new Uint8Array(array)], { type: 'image/png' }); - }; + } - GitLabCrop.prototype.setPreview = function() { + setPreview() { var filename; this.previewImage.attr('src', this.dataURL); filename = this.fileInput.val().replace(FILENAMEREGEX, ''); return this.filename.text(filename); - }; + } - GitLabCrop.prototype.setBlob = function() { + setBlob() { this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { width: 200, height: 200 }).toDataURL('image/png'); return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); - }; + } - GitLabCrop.prototype.getBlob = function() { + getBlob() { return this.croppedImageBlob; - }; - - return GitLabCrop; - - })(); + } + } $.fn.glCrop = function(opts) { return this.each(function() { return $(this).data('glcrop', new GitLabCrop(this, opts)); }); - }; + } -}).call(this); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js deleted file mode 100644 index 60f9fba5777..00000000000 --- a/app/assets/javascripts/profile/profile.js +++ /dev/null @@ -1,106 +0,0 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Profile = (function() { - function Profile(opts) { - var cropOpts, ref; - if (opts == null) { - opts = {}; - } - this.onSubmitForm = bind(this.onSubmitForm, this); - this.form = (ref = opts.form) != null ? ref : $('.edit-user'); - $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() { - return $(this).parents('form').submit(); - // Automatically submit the Preferences form when any of its radio buttons change - }); - $('#user_notification_email').on('change', function() { - return $(this).parents('form').submit(); - // Automatically submit email form when it changes - }); - $('.update-username').on('ajax:before', function() { - $('.loading-username').show(); - $(this).find('.update-success').hide(); - return $(this).find('.update-failed').hide(); - }); - $('.update-username').on('ajax:complete', function() { - $('.loading-username').hide(); - $(this).find('.btn-save').enable(); - return $(this).find('.loading-gif').hide(); - }); - $('.update-notifications').on('ajax:success', function(e, data) { - if (data.saved) { - return new Flash("Notification settings saved", "notice"); - } else { - return new Flash("Failed to save new settings", "alert"); - } - }); - this.bindEvents(); - cropOpts = { - filename: '.js-avatar-filename', - previewImage: '.avatar-image .avatar', - modalCrop: '.modal-profile-crop', - pickImageEl: '.js-choose-user-avatar-button', - uploadImageBtn: '.js-upload-user-avatar', - modalCropImg: '.modal-profile-crop-image' - }; - this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); - } - - Profile.prototype.bindEvents = function() { - return this.form.on('submit', this.onSubmitForm); - }; - - Profile.prototype.onSubmitForm = function(e) { - e.preventDefault(); - return this.saveForm(); - }; - - Profile.prototype.saveForm = function() { - var avatarBlob, formData, self; - self = this; - formData = new FormData(this.form[0]); - avatarBlob = this.avatarGlCrop.getBlob(); - if (avatarBlob != null) { - formData.append('user[avatar]', avatarBlob, 'avatar.png'); - } - return $.ajax({ - url: this.form.attr('action'), - type: this.form.attr('method'), - data: formData, - dataType: "json", - processData: false, - contentType: false, - success: function(response) { - return new Flash(response.message, 'notice'); - }, - error: function(jqXHR) { - return new Flash(jqXHR.responseJSON.message, 'alert'); - }, - complete: function() { - window.scrollTo(0, 0); - // Enable submit button after requests ends - return self.form.find(':input[disabled]').enable(); - } - }); - }; - - return Profile; - - })(); - - $(function() { - $(document).on('focusout.ssh_key', '#key_key', function() { - var $title, comment; - $title = $('#key_title'); - comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); - if (comment && comment.length > 1 && $title.val() === '') { - return $title.val(comment[1]).change(); - } - // Extract the SSH Key title from its comment - }); - if (gl.utils.getPagePath() === 'profiles') { - return new Profile(); - } - }); - -}).call(this); diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 new file mode 100644 index 00000000000..b2307be73ad --- /dev/null +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -0,0 +1,100 @@ +((global) => { + + class Profile { + constructor({ form } = {}) { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.form = form || $('.edit-user'); + this.bindEvents(); + this.initAvatarGlCrop(); + } + + initAvatarGlCrop() { + const cropOpts = { + filename: '.js-avatar-filename', + previewImage: '.avatar-image .avatar', + modalCrop: '.modal-profile-crop', + pickImageEl: '.js-choose-user-avatar-button', + uploadImageBtn: '.js-upload-user-avatar', + modalCropImg: '.modal-profile-crop-image' + }; + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + } + + bindEvents() { + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); + $('#user_notification_email').on('change', this.submitForm); + $('.update-username').on('ajax:before', this.beforeUpdateUsername); + $('.update-username').on('ajax:complete', this.afterUpdateUsername); + $('.update-notifications').on('ajax:success', this.onUpdateNotifs); + this.form.on('submit', this.onSubmitForm); + } + + submitForm() { + return $(this).parents('form').submit(); + } + + onSubmitForm(e) { + e.preventDefault(); + return this.saveForm(); + } + + beforeUpdateUsername() { + $('.loading-username').show(); + $(this).find('.update-success').hide(); + return $(this).find('.update-failed').hide(); + } + + afterUpdateUsername() { + $('.loading-username').hide(); + $(this).find('.btn-save').enable(); + return $(this).find('.loading-gif').hide(); + } + + onUpdateNotifs(e, data) { + return data.saved ? + new Flash("Notification settings saved", "notice") : + new Flash("Failed to save new settings", "alert"); + } + + saveForm() { + const self = this; + const formData = new FormData(this.form[0]); + const avatarBlob = this.avatarGlCrop.getBlob(); + + if (avatarBlob != null) { + formData.append('user[avatar]', avatarBlob, 'avatar.png'); + } + + return $.ajax({ + url: this.form.attr('action'), + type: this.form.attr('method'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + success: response => new Flash(response.message, 'notice'), + error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'), + complete: () => { + window.scrollTo(0, 0); + // Enable submit button after requests ends + return self.form.find(':input[disabled]').enable(); + } + }); + } + } + + $(function() { + $(document).on('focusout.ssh_key', '#key_key', function() { + const $title = $('#key_title'); + const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + if (comment && comment.length > 1 && $title.val() === '') { + return $title.val(comment[1]).change(); + } + // Extract the SSH Key title from its comment + }); + if (global.utils.getPagePath() === 'profiles') { + return new Profile(); + } + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 20b147500cf..4239ed2f889 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -23,7 +23,7 @@ data = groups.concat(projects); return finalCallback(data); }; - return Api.groups(term, false, groupsCallback); + return Api.groups(term, false, false, groupsCallback); }; } else { projectsCallback = finalCallback; @@ -72,7 +72,7 @@ data = groups.concat(projects); return finalCallback(data); }; - return Api.groups(query.term, false, groupsCallback); + return Api.groups(query.term, false, false, groupsCallback); }; } else { projectsCallback = finalCallback; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index d34346f862b..8074a94f33e 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -10,7 +10,7 @@ filterable: true, fieldName: 'group_id', data: function(term, callback) { - return Api.groups(term, null, function(data) { + return Api.groups(term, false, false, function(data) { data.unshift({ name: 'Any' }); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js.es6 index 8abb09c626f..b4c6226dc68 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,30 +1,21 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.SearchAutocomplete = (function() { - var KEYCODE; - - KEYCODE = { - ESCAPE: 27, - BACKSPACE: 8, - ENTER: 13, - UP: 38, - DOWN: 40 - }; +((global) => { - function SearchAutocomplete(opts) { - var ref, ref1, ref2, ref3, ref4; - if (opts == null) { - opts = {}; - } - this.onSearchInputBlur = bind(this.onSearchInputBlur, this); - this.onClearInputClick = bind(this.onClearInputClick, this); - this.onSearchInputFocus = bind(this.onSearchInputFocus, this); - this.onSearchInputClick = bind(this.onSearchInputClick, this); - this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this); - this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this); - this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || ''; - // Dropdown Element + const KEYCODE = { + ESCAPE: 27, + BACKSPACE: 8, + ENTER: 13, + UP: 38, + DOWN: 40 + }; + + class SearchAutocomplete { + constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { + this.bindEventContext(); + this.wrap = wrap || $('.search'); + this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); + this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); + this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); + this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); this.dropdown = this.wrap.find('.dropdown'); this.dropdownContent = this.dropdown.find('.dropdown-content'); this.locationBadgeEl = this.getElement('.location-badge'); @@ -46,19 +37,27 @@ } // Finds an element inside wrapper element - SearchAutocomplete.prototype.getElement = function(selector) { + bindEventContext() { + this.onSearchInputBlur = this.onSearchInputBlur.bind(this); + this.onClearInputClick = this.onClearInputClick.bind(this); + this.onSearchInputFocus = this.onSearchInputFocus.bind(this); + this.onSearchInputClick = this.onSearchInputClick.bind(this); + this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); + this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); + } + getElement(selector) { return this.wrap.find(selector); - }; + } - SearchAutocomplete.prototype.saveOriginalState = function() { + saveOriginalState() { return this.originalState = this.serializeState(); - }; + } - SearchAutocomplete.prototype.saveTextLength = function() { + saveTextLength() { return this.lastTextLength = this.searchInput.val().length; - }; + } - SearchAutocomplete.prototype.createAutocomplete = function() { + createAutocomplete() { return this.searchInput.glDropdown({ filterInputBlur: false, filterable: true, @@ -73,9 +72,9 @@ selectable: true, clicked: this.onClick.bind(this) }); - }; + } - SearchAutocomplete.prototype.getData = function(term, callback) { + getData(term, callback) { var _this, contents, jqXHR; _this = this; if (!term) { @@ -138,9 +137,9 @@ }).always(function() { return _this.loadingSuggestions = false; }); - }; + } - SearchAutocomplete.prototype.getCategoryContents = function() { + getCategoryContents() { var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; userId = gon.current_user_id; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; @@ -173,9 +172,9 @@ items.splice(0, 1); } return items; - }; + } - SearchAutocomplete.prototype.serializeState = function() { + serializeState() { return { // Search Criteria search_project_id: this.projectInputEl.val(), @@ -186,9 +185,9 @@ // Location badge _location: this.locationBadgeEl.text() }; - }; + } - SearchAutocomplete.prototype.bindEvents = function() { + bindEvents() { this.searchInput.on('keydown', this.onSearchInputKeyDown); this.searchInput.on('keyup', this.onSearchInputKeyUp); this.searchInput.on('click', this.onSearchInputClick); @@ -200,9 +199,9 @@ return _this.searchInput.focus(); }; })(this)); - }; + } - SearchAutocomplete.prototype.enableAutocomplete = function() { + enableAutocomplete() { var _this; // No need to enable anything if user is not logged in if (!gon.current_user_id) { @@ -216,12 +215,12 @@ } }; - SearchAutocomplete.prototype.onSearchInputKeyDown = function() { // Saves last length of the entered text + onSearchInputKeyDown() { return this.saveTextLength(); - }; + } - SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { + onSearchInputKeyUp(e) { switch (e.keyCode) { case KEYCODE.BACKSPACE: // when trying to remove the location badge @@ -259,54 +258,53 @@ } } this.wrap.toggleClass('has-value', !!e.target.value); - }; + } // Avoid falsy value to be returned - SearchAutocomplete.prototype.onSearchInputClick = function(e) { - // Prevents closing the dropdown menu + onSearchInputClick(e) { return e.stopImmediatePropagation(); - }; + } - SearchAutocomplete.prototype.onSearchInputFocus = function() { + onSearchInputFocus() { this.isFocused = true; this.wrap.addClass('search-active'); if (this.getValue() === '') { return this.getData(); } - }; + } - SearchAutocomplete.prototype.getValue = function() { + getValue() { return this.searchInput.val(); - }; + } - SearchAutocomplete.prototype.onClearInputClick = function(e) { + onClearInputClick(e) { e.preventDefault(); return this.searchInput.val('').focus(); - }; + } - SearchAutocomplete.prototype.onSearchInputBlur = function(e) { + onSearchInputBlur(e) { this.isFocused = false; this.wrap.removeClass('search-active'); // If input is blank then restore state if (this.searchInput.val() === '') { return this.restoreOriginalState(); } - }; + } - SearchAutocomplete.prototype.addLocationBadge = function(item) { + addLocationBadge(item) { var badgeText, category, value; category = item.category != null ? item.category + ": " : ''; value = item.value != null ? item.value : ''; badgeText = "" + category + value; this.locationBadgeEl.text(badgeText).show(); return this.wrap.addClass('has-location-badge'); - }; + } - SearchAutocomplete.prototype.hasLocationBadge = function() { + hasLocationBadge() { return this.wrap.is('.has-location-badge'); }; - SearchAutocomplete.prototype.restoreOriginalState = function() { + restoreOriginalState() { var i, input, inputs, len; inputs = Object.keys(this.originalState); for (i = 0, len = inputs.length; i < len; i++) { @@ -320,13 +318,13 @@ value: this.originalState._location }); } - }; + } - SearchAutocomplete.prototype.badgePresent = function() { + badgePresent() { return this.locationBadgeEl.length; - }; + } - SearchAutocomplete.prototype.resetSearchState = function() { + resetSearchState() { var i, input, inputs, len, results; inputs = Object.keys(this.originalState); results = []; @@ -339,30 +337,30 @@ results.push(this.getElement("#" + input).val('')); } return results; - }; + } - SearchAutocomplete.prototype.removeLocationBadge = function() { + removeLocationBadge() { this.locationBadgeEl.hide(); this.resetSearchState(); this.wrap.removeClass('has-location-badge'); return this.disableAutocomplete(); - }; + } - SearchAutocomplete.prototype.disableAutocomplete = function() { + disableAutocomplete() { if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { this.searchInput.addClass('disabled'); this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); this.restoreMenu(); } - }; + } - SearchAutocomplete.prototype.restoreMenu = function() { + restoreMenu() { var html; html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; return this.dropdownContent.html(html); }; - SearchAutocomplete.prototype.onClick = function(item, $el, e) { + onClick(item, $el, e) { if (location.pathname.indexOf(item.url) !== -1) { e.preventDefault(); if (!this.badgePresent) { @@ -385,8 +383,45 @@ } }; - return SearchAutocomplete; + } + + global.SearchAutocomplete = SearchAutocomplete; + + $(function() { + var $projectOptionsDataEl = $('.js-search-project-options'); + var $groupOptionsDataEl = $('.js-search-group-options'); + var $dashboardOptionsDataEl = $('.js-search-dashboard-options'); + + if ($projectOptionsDataEl.length) { + gl.projectOptions = gl.projectOptions || {}; + + var projectPath = $projectOptionsDataEl.data('project-path'); + + gl.projectOptions[projectPath] = { + name: $projectOptionsDataEl.data('name'), + issuesPath: $projectOptionsDataEl.data('issues-path'), + mrPath: $projectOptionsDataEl.data('mr-path') + }; + } - })(); + if ($groupOptionsDataEl.length) { + gl.groupOptions = gl.groupOptions || {}; + + var groupPath = $groupOptionsDataEl.data('group-path'); + + gl.groupOptions[groupPath] = { + name: $groupOptionsDataEl.data('name'), + issuesPath: $groupOptionsDataEl.data('issues-path'), + mrPath: $groupOptionsDataEl.data('mr-path') + }; + } + + if ($dashboardOptionsDataEl.length) { + gl.dashboardOptions = { + issuesPath: $dashboardOptionsDataEl.data('issues-path'), + mrPath: $dashboardOptionsDataEl.data('mr-path') + }; + } + }); -}).call(this); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 156b9b8abec..ee6af123268 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -10,12 +10,13 @@ ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; - COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'; + 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) { this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); + this.$toggleIcon = $('.diff-toggle-caret', this.file); this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); this.isOpen = !this.diffForPath; if (this.diffForPath) { @@ -23,18 +24,22 @@ this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); this.content = null; this.collapsedContent.after(this.loadingContent); + this.$toggleIcon.addClass('fa-caret-right'); } else { this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); this.content.after(this.collapsedContent); + this.$toggleIcon.addClass('fa-caret-down'); } - this.collapsedContent.on('click', this.toggleDiff); - $('.file-title > a', this.file).on('click', this.toggleDiff); + $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); } SingleFileDiff.prototype.toggleDiff = function(e) { + var $target = $(e.target); + 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) { this.content.hide(); + this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); this.collapsedContent.show(); if (typeof DiffNotesApp !== 'undefined') { DiffNotesApp.compileComponents(); @@ -42,10 +47,12 @@ } else if (this.content) { this.collapsedContent.hide(); this.content.show(); + this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); if (typeof DiffNotesApp !== 'undefined') { DiffNotesApp.compileComponents(); } } else { + this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); return this.getContentHTML(); } }; diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index c32ddf80219..2ecf3b18975 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,7 +1,7 @@ /*= require ../blob/template_selector */ ((global) => { - class IssuableTemplateSelector extends TemplateSelector { + class IssuableTemplateSelector extends gl.TemplateSelector { constructor(...args) { super(...args); this.projectPath = this.dropdown.data('project-path'); @@ -16,7 +16,7 @@ if (initialQuery.name) this.requestFile(initialQuery); $('.reset-template', this.dropdown.parent()).on('click', () => { - if (this.currentTemplate) this.setInputValueToTemplateContent(); + if (this.currentTemplate) this.setInputValueToTemplateContent(false); }); } @@ -26,26 +26,28 @@ this.currentTemplate = currentTemplate; if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); - this.setInputValueToTemplateContent(); + this.setInputValueToTemplateContent(true); }); return; } - setInputValueToTemplateContent() { + setInputValueToTemplateContent(append) { // `this.requestFileSuccess` sets the value of the description input field - // to the content of the template selected. + // to the content of the template selected. If `append` is true, the + // template content will be appended to the previous value of the field, + // separated by a blank line if the previous value is non-empty. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and - // skip focusing the description input by setting `true` as the 2nd - // argument to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, true); + // skip focusing the description input by setting `true` as the + // `skipFocus` option to `requestFileSuccess`. + this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append}); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate); + this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append}); } return; } } global.IssuableTemplateSelector = IssuableTemplateSelector; -})(window); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index bd8cdde033e..4e8247b89e1 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,12 +1,12 @@ ((global) => { class IssuableTemplateSelectors { - constructor(opts = {}) { - this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); - this.editor = opts.editor || this.initEditor(); + constructor({ $dropdowns, editor } = {}) { + this.$dropdowns = $dropdowns || $('.js-issuable-selector'); + this.editor = editor || this.initEditor(); this.$dropdowns.each((i, dropdown) => { - let $dropdown = $(dropdown); - new IssuableTemplateSelector({ + const $dropdown = $(dropdown); + new gl.IssuableTemplateSelector({ pattern: /(\.md)/, data: $dropdown.data('data'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'), @@ -26,4 +26,4 @@ } global.IssuableTemplateSelectors = IssuableTemplateSelectors; -})(window); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js.es6 index 93421649ac7..055228c5df8 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js.es6 @@ -1,34 +1,29 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Todos = (function() { - function Todos(opts) { - var ref; - if (opts == null) { - opts = {}; - } - this.allDoneClicked = bind(this.allDoneClicked, this); - this.doneClicked = bind(this.doneClicked, this); - this.el = (ref = opts.el) != null ? ref : $('.js-todos-options'); +((global) => { + + class Todos { + constructor({ el } = {}) { + this.allDoneClicked = this.allDoneClicked.bind(this); + this.doneClicked = this.doneClicked.bind(this); + this.el = el || $('.js-todos-options'); this.perPage = this.el.data('perPage'); this.clearListeners(); this.initBtnListeners(); this.initFilters(); } - Todos.prototype.clearListeners = function() { + clearListeners() { $('.done-todo').off('click'); $('.js-todos-mark-all').off('click'); return $('.todo').off('click'); - }; + } - Todos.prototype.initBtnListeners = function() { + initBtnListeners() { $('.done-todo').on('click', this.doneClicked); $('.js-todos-mark-all').on('click', this.allDoneClicked); return $('.todo').on('click', this.goToTodoUrl); - }; + } - Todos.prototype.initFilters = function() { + initFilters() { new UsersSelect(); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-type-search'), 'type'); @@ -38,125 +33,117 @@ event.preventDefault(); Turbolinks.visit(this.action + '&' + $(this).serialize()); }); - }; + } - Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { + initFilterDropdown($dropdown, fieldName, searchFields) { $dropdown.glDropdown({ + fieldName, selectable: true, filterable: searchFields ? true : false, - fieldName: fieldName, search: { fields: searchFields }, data: $dropdown.data('data'), clicked: function() { return $dropdown.closest('form.filter-form').submit(); } }) - }; + } - Todos.prototype.doneClicked = function(e) { - var $this; + doneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $this = $(e.currentTarget); - $this.disable(); + const $target = $(e.currentTarget); + $target.disable(); return $.ajax({ type: 'POST', - url: $this.attr('href'), + url: $target.attr('href'), dataType: 'json', data: { '_method': 'delete' }, - success: (function(_this) { - return function(data) { - _this.redirectIfNeeded(data.count); - _this.clearDone($this.closest('li')); - return _this.updateBadges(data); - }; - })(this) + success: (data) => { + this.redirectIfNeeded(data.count); + this.clearDone($target.closest('li')); + return this.updateBadges(data); + } }); - }; + } - Todos.prototype.allDoneClicked = function(e) { - var $this; + allDoneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $this = $(e.currentTarget); - $this.disable(); + $target = $(e.currentTarget); + $target.disable(); return $.ajax({ type: 'POST', - url: $this.attr('href'), + url: $target.attr('href'), dataType: 'json', data: { '_method': 'delete' }, - success: (function(_this) { - return function(data) { - $this.remove(); - $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); - return _this.updateBadges(data); - }; - })(this) + success: (data) => { + $target.remove(); + $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); + return this.updateBadges(data); + } }); - }; + } - Todos.prototype.clearDone = function($row) { - var $ul; - $ul = $row.closest('ul'); + clearDone($row) { + const $ul = $row.closest('ul'); $row.remove(); if (!$ul.find('li').length) { return $ul.parents('.panel').remove(); } - }; + } - Todos.prototype.updateBadges = function(data) { + updateBadges(data) { $('.todos-pending .badge, .todos-pending-count').text(data.count); return $('.todos-done .badge').text(data.done_count); - }; + } - Todos.prototype.getTotalPages = function() { + getTotalPages() { return this.el.data('totalPages'); - }; + } - Todos.prototype.getCurrentPage = function() { + getCurrentPage() { return this.el.data('currentPage'); - }; + } - Todos.prototype.getTodosPerPage = function() { + getTodosPerPage() { return this.el.data('perPage'); - }; + } + + redirectIfNeeded(total) { + const currPages = this.getTotalPages(); + const currPage = this.getCurrentPage(); - Todos.prototype.redirectIfNeeded = function(total) { - var currPage, currPages, newPages, pageParams, url; - currPages = this.getTotalPages(); - currPage = this.getCurrentPage(); // Refresh if no remaining Todos if (!total) { - location.reload(); + window.location.reload(); return; } // Do nothing if no pagination if (!currPages) { return; } - newPages = Math.ceil(total / this.getTodosPerPage()); - // Includes query strings - url = location.href; - // If new total of pages is different than we have now + + const newPages = Math.ceil(total / this.getTodosPerPage()); + let url = location.href; + if (newPages !== currPages) { // Redirect to previous page if there's one available if (currPages > 1 && currPage === currPages) { - pageParams = { + const pageParams = { page: currPages - 1 }; url = gl.utils.mergeUrlParams(pageParams, url); } return Turbolinks.visit(url); } - }; + } - Todos.prototype.goToTodoUrl = function(e) { - var todoLink; - todoLink = $(this).data('url'); + goToTodoUrl(e) { + const todoLink = $(this).data('url'); if (!todoLink) { return; } @@ -167,10 +154,8 @@ } else { return Turbolinks.visit(todoLink); } - }; - - return Todos; - - })(); + } + } -}).call(this); + global.Todos = Todos; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 6889d3a7491..0f97924d94e 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,7 +1,7 @@ -(global => { +((global) => { global.User = class { - constructor(opts) { - this.opts = opts; + constructor({ action }) { + this.action = action; this.placeProfileAvatarsToTop(); this.initTabs(); this.hideProjectLimitMessage(); @@ -14,9 +14,9 @@ } initTabs() { - return new UserTabs({ + return new global.UserTabs({ parentEl: '.user-profile', - action: this.opts.action + action: this.action }); } diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js deleted file mode 100644 index 8a657780eb6..00000000000 --- a/app/assets/javascripts/user_tabs.js +++ /dev/null @@ -1,188 +0,0 @@ -// UserTabs -// -// Handles persisting and restoring the current tab selection and lazily-loading -// content on the Users#show page. -// -// ### Example Markup -// -// <ul class="nav-links"> -// <li class="activity-tab active"> -// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> -// Activity -// </a> -// </li> -// <li class="groups-tab"> -// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> -// Groups -// </a> -// </li> -// <li class="contributed-tab"> -// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> -// Contributed projects -// </a> -// </li> -// <li class="projects-tab"> -// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> -// Personal projects -// </a> -// </li> -// <li class="snippets-tab"> -// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> -// </a> -// </li> -// </ul> -// -// <div class="tab-content"> -// <div class="tab-pane" id="activity"> -// Activity Content -// </div> -// <div class="tab-pane" id="groups"> -// Groups Content -// </div> -// <div class="tab-pane" id="contributed"> -// Contributed projects content -// </div> -// <div class="tab-pane" id="projects"> -// Projects content -// </div> -// <div class="tab-pane" id="snippets"> -// Snippets content -// </div> -// </div> -// -// <div class="loading-status"> -// <div class="loading"> -// Loading Animation -// </div> -// </div> -// -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.UserTabs = (function() { - function UserTabs(opts) { - this.tabShown = bind(this.tabShown, this); - var i, item, len, ref, ref1, ref2, ref3; - this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document); - // Make jQuery object if selector is provided - if (typeof this.parentEl === 'string') { - this.parentEl = $(this.parentEl); - } - // Store the `location` object, allowing for easier stubbing in tests - this._location = location; - // Set tab states - this.loaded = {}; - ref3 = this.parentEl.find('.nav-links a'); - for (i = 0, len = ref3.length; i < len; i++) { - item = ref3[i]; - this.loaded[$(item).attr('data-action')] = false; - } - // Actions - this.actions = Object.keys(this.loaded); - this.bindEvents(); - // Set active tab - if (this.action === 'show') { - this.action = this.defaultAction; - } - this.activateTab(this.action); - } - - UserTabs.prototype.bindEvents = function() { - // Toggle event listeners - return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown); - }; - - UserTabs.prototype.tabShown = function(event) { - var $target, action, source; - $target = $(event.target); - action = $target.data('action'); - source = $target.attr('href'); - this.setTab(source, action); - return this.setCurrentAction(action); - }; - - UserTabs.prototype.activateTab = function(action) { - return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show'); - }; - - UserTabs.prototype.setTab = function(source, action) { - if (this.loaded[action] === true) { - return; - } - if (action === 'activity') { - this.loadActivities(source); - } - if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') { - return this.loadTab(source, action); - } - }; - - UserTabs.prototype.loadTab = function(source, action) { - return $.ajax({ - beforeSend: (function(_this) { - return function() { - return _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - return _this.toggleLoading(false); - }; - })(this), - dataType: 'json', - type: 'GET', - url: source + ".json", - success: (function(_this) { - return function(data) { - var tabSelector; - tabSelector = 'div#' + action; - _this.parentEl.find(tabSelector).html(data.html); - _this.loaded[action] = true; - // Fix tooltips - return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); - }; - })(this) - }); - }; - - UserTabs.prototype.loadActivities = function(source) { - var $calendarWrap; - if (this.loaded['activity'] === true) { - return; - } - $calendarWrap = this.parentEl.find('.user-calendar'); - $calendarWrap.load($calendarWrap.data('href')); - new Activities(); - return this.loaded['activity'] = true; - }; - - UserTabs.prototype.toggleLoading = function(status) { - return this.parentEl.find('.loading-status .loading').toggle(status); - }; - - UserTabs.prototype.setCurrentAction = function(action) { - var new_state, regExp; - // Remove possible actions from URL - regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$'); - new_state = this._location.pathname; - // remove trailing slashes - new_state = new_state.replace(/\/+$/, ""); - new_state = new_state.replace(regExp, ''); - // Append the new action if we're on a tab other than 'activity' - if (action !== this.defaultAction) { - new_state += "/" + action; - } - // Ensure parameters and hash come along for the ride - new_state += this._location.search + this._location.hash; - history.replaceState({ - turbolinks: true, - url: new_state - }, document.title, new_state); - return new_state; - }; - - return UserTabs; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 new file mode 100644 index 00000000000..63bce0a6f6f --- /dev/null +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -0,0 +1,162 @@ +/* +UserTabs + +Handles persisting and restoring the current tab selection and lazily-loading +content on the Users#show page. + +### Example Markup + + <ul class="nav-links"> + <li class="activity-tab active"> + <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> + Activity + </a> + </li> + <li class="groups-tab"> + <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> + Groups + </a> + </li> + <li class="contributed-tab"> + <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> + Contributed projects + </a> + </li> + <li class="projects-tab"> + <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> + Personal projects + </a> + </li> + <li class="snippets-tab"> + <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> + </a> + </li> + </ul> + + <div class="tab-content"> + <div class="tab-pane" id="activity"> + Activity Content + </div> + <div class="tab-pane" id="groups"> + Groups Content + </div> + <div class="tab-pane" id="contributed"> + Contributed projects content + </div> + <div class="tab-pane" id="projects"> + Projects content + </div> + <div class="tab-pane" id="snippets"> + Snippets content + </div> + </div> + + <div class="loading-status"> + <div class="loading"> + Loading Animation + </div> + </div> +*/ +((global) => { + class UserTabs { + constructor ({ defaultAction, action, parentEl }) { + this.loaded = {}; + this.defaultAction = defaultAction || 'activity'; + this.action = action || this.defaultAction; + this.$parentEl = $(parentEl) || $(document); + this._location = window.location; + this.$parentEl.find('.nav-links a') + .each((i, navLink) => { + this.loaded[$(navLink).attr('data-action')] = false; + }); + this.actions = Object.keys(this.loaded); + this.bindEvents(); + + if (this.action === 'show') { + this.action = this.defaultAction; + } + + this.activateTab(this.action); + } + + bindEvents() { + return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); + } + + tabShown(event) { + const $target = $(event.target); + const action = $target.data('action'); + const source = $target.attr('href'); + this.setTab(source, action); + return this.setCurrentAction(action); + } + + activateTab(action) { + return this.$parentEl.find(`.nav-links .js-${action}-tab a`) + .tab('show'); + } + + setTab(source, action) { + if (this.loaded[action]) { + return; + } + if (action === 'activity') { + this.loadActivities(source); + } + + const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ]; + if (loadableActions.indexOf(action) > -1) { + return this.loadTab(source, action); + } + } + + loadTab(source, action) { + return $.ajax({ + beforeSend: () => this.toggleLoading(true), + complete: () => this.toggleLoading(false), + dataType: 'json', + type: 'GET', + url: `${source}.json`, + success: (data) => { + const tabSelector = `div#${action}`; + this.$parentEl.find(tabSelector).html(data.html); + this.loaded[action] = true; + return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + } + }); + } + + loadActivities(source) { + if (this.loaded['activity']) { + return; + } + const $calendarWrap = this.$parentEl.find('.user-calendar'); + $calendarWrap.load($calendarWrap.data('href')); + new Activities(); + return this.loaded['activity'] = true; + } + + toggleLoading(status) { + return this.$parentEl.find('.loading-status .loading') + .toggle(status); + } + + setCurrentAction(action) { + const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`); + let new_state = this._location.pathname; + new_state = new_state.replace(/\/+$/, ''); + new_state = new_state.replace(regExp, ''); + if (action !== this.defaultAction) { + new_state += `/${action}`; + } + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + return new_state; + } + } + global.UserTabs = UserTabs; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 9c277998db4..05056a73aaf 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -14,11 +14,12 @@ $('.js-user-search').each((function(_this) { return function(i, dropdown) { var options = {}; - var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; + var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; $dropdown = $(dropdown); options.projectId = $dropdown.data('project-id'); options.showCurrentUser = $dropdown.data('current-user'); showNullUser = $dropdown.data('null-user'); + showMenuAbove = $dropdown.data('showMenuAbove'); showAnyUser = $dropdown.data('any-user'); firstUser = $dropdown.data('first-user'); options.authorId = $dropdown.data('author-id'); @@ -73,6 +74,7 @@ collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, data: function(term, callback) { var isAuthorFilter; isAuthorFilter = $('.js-author-search'); @@ -116,8 +118,11 @@ if (showDivider) { users.splice(showDivider, 0, "divider"); } - // Send the data back - return callback(users); + + callback(users); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } }); }, filterable: true, @@ -127,8 +132,8 @@ }, selectable: true, fieldName: $dropdown.data('field-name'), - toggleLabel: function(selected) { - if (selected && 'id' in selected) { + toggleLabel: function(selected, el) { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { if (selected.text) { return selected.text; } else { @@ -138,6 +143,7 @@ return defaultLabel; } }, + defaultLabel: defaultLabel, inputId: 'issue_assignee_id', hidden: function(e) { $selectbox.hide(); @@ -149,7 +155,9 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update')) { + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); + selectedId = user.id; return; } if (page === 'projects:boards:show') { @@ -167,6 +175,9 @@ return assignTo(selected); } }, + id: function (user) { + return user.id; + }, renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; username = user.username ? "@" + user.username : ""; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 2432ddb72f4..d315db4cb32 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -19,10 +19,8 @@ &.diff-collapsed { padding: 5px; - cursor: pointer; - - &:hover { - background-color: $row-hover; + .click-to-expand { + cursor: pointer; } } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 4618687a4be..ce489f7c3de 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -336,3 +336,9 @@ box-shadow: inset 0 0 0 white; } } + +@media (max-width: $screen-xs-max) { + .btn-wide-on-xs { + width: 100%; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index b0ba112476b..4a87a73a68a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -604,3 +604,9 @@ display: block; color: $gl-placeholder-color; } + +.dropdown-toggle-text { + &.is-default { + color: $gl-placeholder-color; + } +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 76a3c083697..81520500594 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -26,6 +26,15 @@ padding: 10px $gl-padding; word-wrap: break-word; border-radius: 3px 3px 0 0; + cursor: pointer; + + &:hover { + background-color: $dark-background-color; + } + + .diff-toggle-caret { + padding-right: 6px; + } &.file-title-clear { padding-left: 0; diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 7ae309ba103..a55dcf4a699 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -3,6 +3,8 @@ margin: 0; margin-bottom: $gl-padding; font-size: 14px; + position: relative; + z-index: 1; .flash-notice { @extend .alert; @@ -19,7 +21,8 @@ .flash-notice, .flash-alert { border-radius: $border-radius-default; - .container-fluid.container-limited.flash-text { + .container-fluid, + .container-fluid.container-limited { background: transparent; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d4a030f7f7a..c748f856501 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -112,11 +112,15 @@ header { .header-logo { position: absolute; left: 50%; - margin-left: -18px; top: 7px; transition-duration: .3s; z-index: 999; + #logo { + position: relative; + left: -50%; + } + svg, img { height: 36px; } @@ -126,8 +130,12 @@ header { } @media (max-width: $screen-xs-max) { - right: 25px; + right: 20px; left: auto; + + #logo { + left: auto; + } } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 46af18580d5..efc348214c2 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -164,7 +164,7 @@ ul.content-list { } .no-comments { - opacity: 0.5; + opacity: .5; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 3b7de4b57bb..557ef7291cf 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -142,6 +142,7 @@ transition-duration: .3s; position: absolute; top: 0; + cursor: pointer; &:hover, &:focus { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9c84dceed05..ecc5b24e360 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -197,6 +197,7 @@ lex a { color: inherit; + word-wrap: break-word; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index a5a260d4c8f..194a39a8377 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -107,10 +107,14 @@ .block { width: 100%; - } - .block-first { - padding: 5px 16px 11px; + &.coverage { + padding: 0 16px 11px; + } + + .btn-group-justified { + margin-top: 5px; + } } .js-build-variable { @@ -214,6 +218,9 @@ .build-detail-row { margin-bottom: 5px; + &:last-of-type { + margin-bottom: 0; + } } .build-light-text { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 1aa4e06d975..e1304335271 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -59,6 +59,7 @@ } .encoding-selector, + .soft-wrap-toggle, .license-selector, .gitignore-selector, .gitlab-ci-yml-selector { @@ -67,6 +68,24 @@ font-family: $regular_font; } + .soft-wrap-toggle { + margin: 0 $btn-side-margin; + .soft-wrap { + display: block; + } + .no-wrap { + display: none; + } + &.soft-wrap-active { + .soft-wrap { + display: none; + } + .no-wrap { + display: block; + } + } + } + .gitignore-selector, .license-selector, .gitlab-ci-yml-selector { .dropdown { line-height: 21px; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 732dc645c66..185ce970e71 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -57,7 +57,6 @@ } .groups-header { - @media (min-width: $screen-sm-min) { .nav-links { width: 35%; @@ -68,3 +67,38 @@ } } } + +.groups-empty-state { + padding: 50px 100px; + overflow: hidden; + + @media (max-width: $screen-md-min) { + padding: 50px 0; + } + + svg { + float: right; + + @media (max-width: $screen-md-min) { + float: none; + display: block; + width: 250px; + position: relative; + left: 50%; + margin-left: -125px; + } + } + + .text-content { + float: left; + width: 460px; + margin-top: 120px; + + @media (max-width: $screen-md-min) { + float: none; + margin-top: 60px; + width: auto; + text-align: center; + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 926247e5e87..bc8693ae467 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -70,7 +70,8 @@ &.ci-success { color: $gl-success; - a.environment { + a.environment, + a.pipeline { color: inherit; } } @@ -349,6 +350,10 @@ .issuable-form-select-holder { display: inline-block; width: 250px; + + .dropdown-menu-toggle { + width: 100%; + } } .table-holder { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 6b865730487..8c2ba3ed58c 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -33,6 +33,7 @@ // Issue title span a { color: $gl-text-color; + word-wrap: break-word; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b035bfc9f3c..68fc6da6c1b 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -22,6 +22,11 @@ .table.builds { min-width: 1200px; + + .branch-commit { + width: 33%; + } + } } @@ -385,6 +390,8 @@ left: auto; right: -214px; top: -9px; + max-height: 245px; + overflow-y: scroll; a:hover { .ci-status-text { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 8c8c403244e..87548dcb590 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -146,7 +146,8 @@ } .project-repo-btn-group, - .notification-dropdown { + .notification-dropdown, + .project-dropdown { margin-left: 10px; } @@ -743,6 +744,62 @@ pre.light-well { .dropdown-menu { width: 300px; } + + &.from .compare-dropdown-toggle { + width: 237px; + } + + &.to .compare-dropdown-toggle { + width: 254px; + } + + .dropdown-toggle-text { + display: block; + height: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } +} + +.compare-ellipsis { + display: inline; +} + +@media (max-width: $screen-xs-max) { + .compare-form-group { + .input-group { + width: 100%; + + & > .compare-dropdown-toggle { + width: 100%; + } + } + + .dropdown-menu { + width: 100%; + } + } + + .compare-switch-container { + text-align: center; + padding: 0 0 $gl-padding; + + .commits-compare-switch { + float: none; + } + } + + .compare-ellipsis { + display: block; + text-align: center; + padding: 0 0 $gl-padding; + } + + .commits-compare-btn { + width: 100%; + } } .clearable-input { @@ -779,4 +836,4 @@ pre.light-well { border-top-right-radius: 0; border-bottom-right-radius: 0; } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 436fb00ba2e..e77f9816d8a 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -103,7 +103,7 @@ // Custom dropdown positioning .dropdown-menu { - top: 30px; + top: 37px; left: -5px; padding: 0; diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 4d5df566d9b..857eb76131a 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -36,3 +36,7 @@ float: right; } } + +.snippet-scope-menu .btn-new { + margin-top: 15px; +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 68a5d1ae06c..ea76fe18876 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -51,6 +51,7 @@ -webkit-flex-direction: column; flex-direction: column; margin-left: 10px; + min-width: 55px; } .todo-item { @@ -120,6 +121,14 @@ } } +@media (max-width: $screen-sm-max) { + .todos-filters { + .dropdown-menu-toggle { + width: 135px; + } + } +} + @media (max-width: $screen-xs-max) { .todo { .avatar { @@ -141,4 +150,14 @@ padding-left: 10px; } } + + .todos-filters { + .row-content-block { + padding-bottom: 50px; + } + + .dropdown-menu-toggle { + width: 100%; + } + } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 7b6577c513e..41ad10f07bd 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -27,7 +27,12 @@ } .last-commit { - @include str-truncated(60%); + @include str-truncated(506px); + + @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) { + @include str-truncated(450px); + } + } .commit-history-link-spacer { diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index aed77d0358a..aa7570cd896 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController def show @members = @group.members.order("access_level DESC").page(params[:members_page]) - @requesters = @group.requesters + @requesters = AccessRequestsFinder.new(@group).execute(current_user) @projects = @group.projects.page(params[:projects_page]) end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 0d2f4f6eb38..1d963bdf7d5 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController end @project_members = @project.members.page(params[:project_members_page]) - @requesters = @project.requesters + @requesters = AccessRequestsFinder.new(@project).execute(current_user) end def transfer diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index e06d12cfce1..78012960252 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -14,6 +14,7 @@ module Ci @config_processor = Ci::GitlabCiYamlProcessor.new(@content) @stages = @config_processor.stages @builds = @config_processor.builds + @jobs = @config_processor.jobs end rescue @error = 'Undefined error' diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index d5a8a962662..4c497711fc0 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor # # Returns nil def prompt_for_two_factor(user) + return locked_user_redirect(user) if user.access_locked? + session[:otp_user_id] = user.id setup_u2f_authentication(user) render 'devise/sessions/two_factor' end + def locked_user_redirect(user) + flash.now[:alert] = 'Invalid Login or password' + render 'devise/sessions/new' + end + def authenticate_with_two_factor user = self.resource = find_user - if user_params[:otp_attempt].present? && session[:otp_user_id] + if user.access_locked? + locked_user_redirect(user) + elsif user_params[:otp_attempt].present? && session[:otp_user_id] authenticate_with_two_factor_via_otp(user) elsif user_params[:device_response].present? && session[:otp_user_id] authenticate_with_two_factor_via_u2f(user) @@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else + user.increment_failed_attempts! flash.now[:alert] = 'Invalid two-factor code.' - render :two_factor + prompt_for_two_factor(user) end end @@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else + user.increment_failed_attempts! flash.now[:alert] = 'Authentication via U2F device failed.' prompt_for_two_factor(user) end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 52682ef9dc9..c13333641d3 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -1,6 +1,5 @@ module MembershipActions extend ActiveSupport::Concern - include MembersHelper def request_access membershipable.request_access(current_user) @@ -10,28 +9,23 @@ module MembershipActions end def approve_access_request - @member = membershipable.requesters.find(params[:id]) - - return render_403 unless can?(current_user, action_member_permission(:update, @member), @member) - - @member.accept_request + Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute redirect_to polymorphic_url([membershipable, :members]) end def leave - @member = membershipable.members.find_by(user_id: current_user) || - membershipable.requesters.find_by(user_id: current_user) - Members::DestroyService.new(@member, current_user).execute + member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). + execute(:all) - source_type = @member.real_source_type.humanize(capitalize: false) + source_type = membershipable.class.to_s.humanize(capitalize: false) notice = - if @member.request? + if member.request? "Your access request to the #{source_type} has been withdrawn." else - "You left the \"#{@member.source.human_name}\" #{source_type}." + "You left the \"#{membershipable.human_name}\" #{source_type}." end - redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] + redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] redirect_to redirect_path, notice: notice end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 29e243c66a3..99acd98ae13 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -7,7 +7,7 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully." + redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." else redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 88a0c18180b..38e5943eb76 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = TrendingProjectsFinder.new.execute(current_user) + @projects = TrendingProjectsFinder.new.execute @projects = filter_projects(@projects) @projects = @projects.page(params[:page]) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 272164cd0cc..18cd800c619 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end @members = @members.order('access_level DESC').page(params[:page]).per(50) - @requesters = @group.requesters if can?(current_user, :admin_group, @group) + @requesters = AccessRequestsFinder.new(@group).execute(current_user) @group_member = @group.group_members.new end @@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end def destroy - @group_member = @group.members.find_by(id: params[:id]) || - @group.requesters.find_by(id: params[:id]) - - Members::DestroyService.new(@group_member, current_user).execute + Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all) respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 7d0eff37635..3ec173abcdb 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -1,6 +1,5 @@ class Import::GitlabProjectsController < Import::BaseController before_action :verify_gitlab_project_import_enabled - before_action :authenticate_admin! def new @namespace_id = project_params[:namespace_id] @@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController :path, :namespace_id, :file ) end - - def authenticate_admin! - render_404 unless current_user.is_admin? - end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 34d5d99558e..7e4da73bc11 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -25,7 +25,7 @@ class JwtController < ApplicationController authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) - render_403 unless @authentication_result.success? && + render_unauthorized unless @authentication_result.success? && (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) end rescue Gitlab::Auth::MissingPersonalTokenError @@ -33,10 +33,21 @@ class JwtController < ApplicationController end def render_missing_personal_token - render plain: "HTTP Basic: Access denied\n" \ - "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \ - "You can generate one at #{profile_personal_access_tokens_url}", - status: 401 + render json: { + errors: [ + { code: 'UNAUTHORIZED', + message: "HTTP Basic: Access denied\n" \ + "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \ + "You can generate one at #{profile_personal_access_tokens_url}" } + ] }, status: 401 + end + + def render_unauthorized + render json: { + errors: [ + { code: 'UNAUTHORIZED', + message: 'HTTP Basic: Access denied' } + ] }, status: 401 end def auth_params diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index c5fa756d02b..f71e0a1302b 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -73,7 +73,8 @@ class ProfilesController < Profiles::ApplicationController :skype, :twitter, :username, - :website_url + :website_url, + :organization ) end end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 9404612a993..4aa7982eab4 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -33,7 +33,7 @@ module Projects def issue @issue ||= - IssuesFinder.new(current_user, project_id: project.id, state: 'all') + IssuesFinder.new(current_user, project_id: project.id) .execute .where(iid: params[:id]) .first! diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 33206717089..0035633b774 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,4 +1,6 @@ class Projects::BoardsController < Projects::ApplicationController + include IssuableCollections + respond_to :html before_action :authorize_read_board!, only: [:show] diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 02fb3f56890..cdfc1ba7b92 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -10,10 +10,11 @@ class Projects::CommitController < Projects::ApplicationController 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_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] - before_action :define_status_vars, only: [:show, :builds] + before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines] + before_action :define_status_vars, only: [:show, :builds, :pipelines] before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] @@ -31,6 +32,9 @@ class Projects::CommitController < Projects::ApplicationController render_diff_for_path(@commit.diffs(diff_options)) end + def pipelines + end + def builds end @@ -96,10 +100,6 @@ class Projects::CommitController < Projects::ApplicationController @noteable = @commit ||= @project.commit(params[:id]) end - def pipelines - @pipelines ||= project.pipelines.where(sha: commit.sha) - end - def ci_builds @ci_builds ||= Ci::Build.where(pipeline: pipelines) end @@ -134,8 +134,9 @@ class Projects::CommitController < Projects::ApplicationController end def define_status_vars - @statuses = CommitStatus.where(pipeline: pipelines).relevant - @builds = Ci::Build.where(pipeline: pipelines).relevant + @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/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index d0c4550733c..7a7475a7345 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -4,17 +4,25 @@ class Projects::GroupLinksController < Projects::ApplicationController def index @group_links = project.project_group_links.all + + @skip_groups = @group_links.pluck(:group_id) + @skip_groups << project.group.try(:id) end def create - group = Group.find(params[:link_group_id]) - return render_404 unless can?(current_user, :read_group, group) - - project.project_group_links.create( - group: group, - group_access: params[:link_group_access], - expires_at: params[:expires_at] - ) + group = Group.find(params[:link_group_id]) if params[:link_group_id].present? + + if group + return render_404 unless can?(current_user, :read_group, group) + + project.project_group_links.create( + group: group, + group_access: params[:link_group_access], + expires_at: params[:expires_at] + ) + else + flash[:alert] = 'Please select a group.' + end redirect_to namespace_project_group_links_path(project.namespace, project) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3eb13a121bf..ef13e0677d2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -54,7 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController end def show - raw_notes = @issue.notes_with_associations.fresh + raw_notes = @issue.notes.inc_relations_for_view.fresh @notes = Banzai::NoteRenderer. render(raw_notes, @project, current_user, @path, @project_wiki, @ref) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 935417d4ae8..8c8c56228ad 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -18,6 +18,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] + before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -275,7 +276,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def remove_wip - MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request) + MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request) redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), notice: "The merge request can now be merged." @@ -308,8 +309,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end - TodoService.new.merge_merge_request(merge_request, current_user) - @merge_request.update(merge_error: nil) if params[:merge_when_build_succeeds].present? @@ -418,10 +417,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def validates_merge_request - # If source project was removed and merge request for some reason - # wasn't close (Ex. mr from fork to origin) - return invalid_mr if !@merge_request.source_project && @merge_request.open? - # Show git not found page # if there is no saved commits between source & target branch if @merge_request.commits.blank? @@ -496,7 +491,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def invalid_mr - # Render special view for MR with removed source or target branch + # Render special view for MR with removed target branch render 'invalid' end @@ -538,4 +533,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @diff_notes_disabled = !@merge_request_diff.latest? @diffs = @merge_request_diff.diffs(diff_options) end + + def close_merge_request_without_source_project + if !@merge_request.source_project && @merge_request.open? + @merge_request.close + end + end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 42a7e5a2c30..f56b256984b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -29,7 +29,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_members = @group_members.order('access_level DESC') end - @requesters = @project.requesters if can?(current_user, :admin_project, @project) + @requesters = AccessRequestsFinder.new(@project).execute(current_user) @project_member = @project.project_members.new @project_group_links = @project.project_group_links @@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def destroy - @project_member = @project.members.find_by(id: params[:id]) || - @project.requesters.find_by(id: params[:id]) - - Members::DestroyService.new(@project_member, current_user).execute + Members::DestroyService.new(@project, current_user, params). + execute(:all) respond_to do |format| format.html do diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index eaa38fa6c98..62916270172 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController noteable = case params[:type] when 'Issue' - IssuesFinder.new(current_user, project_id: @project.id, state: 'all'). + 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, state: 'all'). + MergeRequestsFinder.new(current_user, project_id: @project.id). execute.find_by(iid: params[:type_id]) when 'Commit' @project.commit(params[:type_id]) @@ -324,7 +324,12 @@ class ProjectsController < Projects::ApplicationController end def repo_exists? - project.repository_exists? && !project.empty_repo? + project.repository_exists? && !project.empty_repo? && project.repo + + rescue Gitlab::Git::Repository::NoRepository + project.repository.expire_exists_cache + + false end def project_view_files? diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a4bedb3bfe6..838ecc837e4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -65,7 +65,7 @@ class UsersController < ApplicationController format.html { render 'show' } format.json do render json: { - html: view_to_html_string("snippets/_snippets", collection: @snippets) + html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true) } end end diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb new file mode 100644 index 00000000000..b6ee49df99b --- /dev/null +++ b/app/finders/access_requests_finder.rb @@ -0,0 +1,27 @@ +class AccessRequestsFinder + attr_accessor :source + + # Arguments: + # source - a Group or Project + def initialize(source) + @source = source + end + + def execute(*args) + execute!(*args) + rescue Gitlab::Access::AccessDeniedError + [] + end + + def execute!(current_user) + raise Gitlab::Access::AccessDeniedError unless can_see_access_requests?(current_user) + + source.requesters + end + + private + + def can_see_access_requests?(current_user) + source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source) + end +end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 8f9ef8f725c..9f170428100 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -183,17 +183,12 @@ class IssuableFinder end def by_state(items) - case params[:state] - when 'closed' - items.closed - when 'merged' - items.respond_to?(:merged) ? items.merged : items.closed - when 'all' - items - when 'opened' - items.opened + params[:state] ||= 'all' + + if items.respond_to?(params[:state]) + items.public_send(params[:state]) else - raise 'You must specify default state' + items end end diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 81a12403801..c1e434d9926 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -1,11 +1,16 @@ +# Finder for retrieving public trending projects in a given time range. class TrendingProjectsFinder - def execute(current_user, start_date = 1.month.ago) - projects_for(current_user).trending(start_date) + # current_user - The currently logged in User, if any. + # last_months - The number of months to limit the trending data to. + def execute(months_limit = 1) + Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do + Project.public_only.trending(months_limit.months.ago) + end end private - def projects_for(current_user) - ProjectsFinder.new.execute(current_user) + def cache_key_for(months) + "trending_projects/#{months}" end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1df430e6279..ebd78bf9888 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -280,32 +280,6 @@ module ApplicationHelper end end - def state_filters_text_for(entity, project) - titles = { - opened: "Open" - } - - entity_title = titles[entity] || entity.to_s.humanize - - count = - if project.nil? - nil - elsif current_controller?(:issues) - project.issues.visible_to_user(current_user).send(entity).count - elsif current_controller?(:merge_requests) - project.merge_requests.send(entity).count - end - - html = content_tag :span, entity_title - - if count.present? - html += " " - html += content_tag :span, number_with_delimiter(count), class: 'badge' - end - - html.html_safe - end - def truncate_first_line(message, length = 50) truncate(message.each_line.first.chomp, length: length) if message end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 639deb7c521..b7f48630bd4 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -56,7 +56,7 @@ module CiStatusHelper def render_commit_status(commit, tooltip_placement: 'auto left') project = commit.project - path = builds_namespace_project_commit_path(project.namespace, project, commit) + path = pipelines_namespace_project_commit_path(project.namespace, project, commit) render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement) end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 4566f3782cc..81e0b6bb5ae 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -40,8 +40,9 @@ module DropdownsHelper end def dropdown_toggle(toggle_text, data_attr, options = {}) + default_label = data_attr[:default_label] content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do - output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") + output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output << icon('chevron-down') output.html_safe end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5c04bba323f..692fadd505f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -8,18 +8,12 @@ module IssuablesHelper end def multi_label_name(current_labels, default_label) - # current_labels may be a string from before - if current_labels.is_a?(Array) - if current_labels.count > 1 - "#{current_labels[0]} +#{current_labels.count - 1} more" + if current_labels && current_labels.any? + title = current_labels.first.try(:title) + if current_labels.size > 1 + "#{title} +#{current_labels.size - 1} more" else - current_labels[0] - end - elsif current_labels.is_a?(String) - if current_labels.nil? || current_labels.empty? - default_label - else - current_labels + title end else default_label @@ -94,6 +88,24 @@ module IssuablesHelper label_names.join(', ') end + def issuables_state_counter_text(issuable_type, state) + titles = { + opened: "Open" + } + + state_title = titles[state] || state.to_s.humanize + + count = + Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do + issuables_count_for_state(issuable_type, state) + end + + html = content_tag(:span, state_title) + html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') + + html.html_safe + end + private def sidebar_gutter_collapsed? @@ -111,4 +123,22 @@ module IssuablesHelper issuable.open? ? :opened : :closed end end + + def issuables_count_for_state(issuable_type, state) + issuables_finder = public_send("#{issuable_type}_finder") + issuables_finder.params[:state] = state + + issuables_finder.execute.page(1).total_count + end + + IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page] + private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY + + def issuables_state_counter_cache_key(issuable_type, state) + opts = params.with_indifferent_access + opts[:state] = state + opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) + + hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) + end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 5e9f5837101..b9f3d6c75c2 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -115,8 +115,9 @@ module LabelsHelper end def labels_filter_path - if @project - namespace_project_labels_path(@project.namespace, @project, :json) + project = @target_project || @project + if project + namespace_project_labels_path(project.namespace, project, :json) else dashboard_labels_path(:json) end diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index c15ecc8f86e..95b60aeab5f 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -1,11 +1,13 @@ module LfsHelper + include Gitlab::Routing.url_helpers + def require_lfs_enabled! return if Gitlab.config.lfs.enabled render( json: { message: 'Git LFS is not enabled on this GitLab server, contact your admin.', - documentation_url: "#{Gitlab.config.gitlab.url}/help", + documentation_url: help_url, }, status: 501 ) @@ -46,7 +48,7 @@ module LfsHelper render( json: { message: 'Access forbidden. Check your access level.', - documentation_url: "#{Gitlab.config.gitlab.url}/help", + documentation_url: help_url, }, content_type: "application/vnd.git-lfs+json", status: 403 @@ -57,7 +59,7 @@ module LfsHelper render( json: { message: 'Not found.', - documentation_url: "#{Gitlab.config.gitlab.url}/help", + documentation_url: help_url, }, content_type: "application/vnd.git-lfs+json", status: 404 diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a11c313a6b8..83a2a4ad3ec 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -71,8 +71,9 @@ module MilestonesHelper end def milestones_filter_dropdown_path - if @project - namespace_project_milestones_path(@project.namespace, @project, :json) + project = @target_project || @project + if project + namespace_project_milestones_path(project.namespace, project, :json) else dashboard_milestones_path(:json) end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 22387d66451..7d4d049101a 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -92,12 +92,8 @@ module PageLayoutHelper end end - def fluid_layout(enabled = false) - if @fluid_layout.nil? - @fluid_layout = (current_user && current_user.layout == "fluid") || enabled - else - @fluid_layout - end + def fluid_layout + current_user && current_user.layout == "fluid" end def blank_container(enabled = false) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 56477733ea2..e667c9e4e2e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -139,7 +139,7 @@ module ProjectsHelper end options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) - content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe + content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe end private diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 5f27e33c6ad..8706876ae4a 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -49,12 +49,10 @@ module SelectsHelper end def select2_tag(id, opts = {}) - css_class = '' - css_class << 'multiselect ' if opts[:multiple] - css_class << (opts[:class] || '') + opts[:class] << ' multiselect' if opts[:multiple] value = opts[:selected] || '' - hidden_field_tag(id, value, class: css_class) + hidden_field_tag(id, value, opts) end private diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 1e86f648203..a9db8bb2b82 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -114,6 +114,26 @@ module TodosHelper selected_type ? selected_type[:text] : default_type end + def todo_due_date(todo) + return unless todo.target.try(:due_date) + + is_due_today = todo.target.due_date.today? + is_overdue = todo.target.overdue? + css_class = + if is_due_today + 'text-warning' + elsif is_overdue + 'text-danger' + else + '' + end + + html = "· ".html_safe + html << content_tag(:span, class: css_class) do + "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}" + end + end + private def show_todo_state?(todo) diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 415f6e12885..f7ed61625f4 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer default reply_to: Gitlab.config.gitlab.email_reply_to layout 'devise_mailer' + + protected + + def subject_for(key) + subject = super + subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? + subject + end end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 45311690293..7b617b359ea 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -45,7 +45,7 @@ module Emails @token = token mail(to: member.invite_email, - subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}") + subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")) end def member_invite_accepted_email(member_source_type, member_id) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 9799f1dc886..2444702104e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -92,6 +92,7 @@ class Notify < BaseMailer subject = "" subject << "#{@project.name} | " if @project subject << extra.join(' | ') if extra.present? + subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? subject end @@ -109,7 +110,7 @@ class Notify < BaseMailer headers['X-GitLab-Reply-Key'] = reply_key if !@labels_url && @sent_notification && @sent_notification.unsubscribable? - headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true) + headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>" @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) end diff --git a/app/models/board.rb b/app/models/board.rb index 3240c4bede3..c56422914a9 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -4,4 +4,12 @@ class Board < ActiveRecord::Base has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all validates :project, presence: true + + def backlog_list + lists.merge(List.backlog).take + end + + def done_list + lists.merge(List.done).take + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 522e2264bb8..5dbf66173de 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -373,7 +373,7 @@ module Ci end def artifacts? - !artifacts_expired? && self[:artifacts_file].present? + !artifacts_expired? && artifacts_file.exists? end def artifacts_metadata? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 663c5b1e231..97df74b0cfe 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -196,7 +196,7 @@ module Ci end def has_warnings? - builds.latest.ignored.any? + builds.latest.failed_but_allowed.any? end def config_processor diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ed5d4b13b7e..44cb19ece3b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,7 +2,7 @@ module Ci class Runner < ActiveRecord::Base extend Ci::Model - LAST_CONTACT_TIME = 2.hours.ago + LAST_CONTACT_TIME = 1.hour.ago AVAILABLE_SCOPES = %w[specific shared active paused online] FORM_EDITABLE = %i[description tag_list active run_untagged locked] diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 656a242c265..ac2477fd973 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -80,7 +80,7 @@ class CommitRange end def inspect - %(#<#{self.class}:#{object_id} #{to_s}>) + %(#<#{self.class}:#{object_id} #{self}>) end def to_s diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 736db1ab0f6..ee3396abe04 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -24,7 +24,22 @@ class CommitStatus < ActiveRecord::Base scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } - scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } + + scope :failed_but_allowed, -> do + where(allow_failure: true, status: [:failed, :canceled]) + end + + scope :exclude_ignored, -> do + quoted_when = connection.quote_column_name('when') + # We want to ignore failed_but_allowed jobs + where("allow_failure = ? OR status IN (?)", + false, all_state_names - [:failed, :canceled]). + # We want to ignore skipped manual jobs + where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped'). + # We want to ignore skipped on_failure + where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped') + end + scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } @@ -111,7 +126,7 @@ class CommitStatus < ActiveRecord::Base end end - def ignored? + def failed_but_allowed? allow_failure? && (failed? || canceled?) end diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb index eedd32a729f..62bc6b809f4 100644 --- a/app/models/concerns/access_requestable.rb +++ b/app/models/concerns/access_requestable.rb @@ -8,9 +8,6 @@ module AccessRequestable extend ActiveSupport::Concern def request_access(user) - members.create( - access_level: Gitlab::Access::DEVELOPER, - user: user, - requested_at: Time.now.utc) + Members::RequestAccessService.new(self, user).execute end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 0fa4df0fb56..9f64f76721d 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -8,32 +8,32 @@ module HasStatus class_methods do def status_sql - scope = all + scope = if respond_to?(:exclude_ignored) + exclude_ignored + else + all + end builds = scope.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql - ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored) - ignored ||= '0' pending = scope.pending.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql - canceled = scope.canceled.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql + canceled = scope.canceled.select('count(*)').to_sql - deduce_status = "(CASE + "(CASE + WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{skipped}) THEN 'skipped' - WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' - WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' - WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' + WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' ELSE 'failed' END)" - - deduce_status end def status - all.pluck(self.status_sql).first + all.pluck(status_sql).first end def started_at @@ -43,6 +43,10 @@ module HasStatus def finished_at all.maximum(:finished_at) end + + def all_state_names + state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) } + end end included do diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index ec9e0f1b1d0..eb2ff0428f6 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -43,19 +43,15 @@ module Mentionable self end - def all_references(current_user = nil, text = nil, extractor: nil) + def all_references(current_user = nil, extractor: nil) extractor ||= Gitlab::ReferenceExtractor. new(project, current_user) - if text - extractor.analyze(text, author: author) - else - self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) - options = options.merge(cache_key: [self, attr], author: author) + self.class.mentionable_attrs.each do |attr, options| + text = __send__(attr) + options = options.merge(cache_key: [self, attr], author: author) - extractor.analyze(text, options) - end + extractor.analyze(text, options) end extractor @@ -66,8 +62,8 @@ module Mentionable end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def referenced_mentionables(current_user = self.author, text = nil) - refs = all_references(current_user, text) + def referenced_mentionables(current_user = self.author) + refs = all_references(current_user) refs = (refs.issues + refs.merge_requests + refs.commits) # We're using this method instead of Array diffing because that requires @@ -77,8 +73,8 @@ module Mentionable end # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. - def create_cross_references!(author = self.author, without = [], text = nil) - refs = referenced_mentionables(author, text) + def create_cross_references!(author = self.author, without = []) + refs = referenced_mentionables(author) # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the @@ -97,10 +93,7 @@ module Mentionable return if changes.empty? - original_text = changes.collect { |_, vals| vals.first }.join(' ') - - preexisting = referenced_mentionables(author, original_text) - create_cross_references!(author, preexisting) + create_cross_references!(author) end private diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index 53b2cacb131..b46db449bf3 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -10,15 +10,33 @@ class CycleAnalytics end def commits - repository = @project.repository.raw_repository - - if @project.default_branch - repository.log(ref: @project.default_branch, after: @from).count - end + 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/deployment.rb b/app/models/deployment.rb index 07d7e19e70d..82b27b78229 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_save :keep_around_commit + after_save :create_ref def commit project.commit(sha) @@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base self == environment.last_deployment end - def keep_around_commit - project.repository.keep_around(self.sha) + def create_ref + project.repository.create_ref(ref, ref_path) end def manual_actions @@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base where.not(id: self.id). take end + + private + + def ref_path + File.join(environment.ref_path, 'deployments', id.to_s) + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 49e0a20640c..f0f3ee23223 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base def update_merge_request_metrics? self.name == "production" end + + def ref_path + "refs/environments/#{Shellwords.shellescape(name)}" + end end diff --git a/app/models/event.rb b/app/models/event.rb index 55a76e26f3c..633019fe0af 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -328,13 +328,15 @@ class Event < ActiveRecord::Base def reset_project_activity return unless project - # Don't even bother obtaining a lock if the last update happened less than - # 60 minutes ago. + # Don't bother updating if we know the project was updated recently. return if recent_update? - return unless try_obtain_lease - - project.update_column(:last_activity_at, created_at) + # At this point it's possible for multiple threads/processes to try to + # update the project. Only one query should actually perform the update, + # hence we add the extra WHERE clause for last_activity_at. + Project.unscoped.where(id: project_id). + where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). + update_all(last_activity_at: created_at) end private @@ -342,11 +344,4 @@ class Event < ActiveRecord::Base def recent_update? project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago end - - def try_obtain_lease - Gitlab::ExclusiveLease. - new("project:update_last_activity_at:#{project.id}", - timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i). - try_obtain - end end diff --git a/app/models/group.rb b/app/models/group.rb index aefb94b2ada..a2f88cca828 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -102,40 +102,44 @@ class Group < Namespace self[:lfs_enabled] end - def add_users(user_ids, access_level, current_user: nil, expires_at: nil) - user_ids.each do |user_id| - Member.add_user( - self.group_members, - user_id, - access_level, - current_user: current_user, - expires_at: expires_at - ) - end + def add_users(users, access_level, current_user: nil, expires_at: nil) + GroupMember.add_users_to_group( + self, + users, + access_level, + current_user: current_user, + expires_at: expires_at + ) end def add_user(user, access_level, current_user: nil, expires_at: nil) - add_users([user], access_level, current_user: current_user, expires_at: expires_at) + GroupMember.add_user( + self, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) end def add_guest(user, current_user = nil) - add_user(user, Gitlab::Access::GUEST, current_user: current_user) + add_user(user, :guest, current_user: current_user) end def add_reporter(user, current_user = nil) - add_user(user, Gitlab::Access::REPORTER, current_user: current_user) + add_user(user, :reporter, current_user: current_user) end def add_developer(user, current_user = nil) - add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user) + add_user(user, :developer, current_user: current_user) end def add_master(user, current_user = nil) - add_user(user, Gitlab::Access::MASTER, current_user: current_user) + add_user(user, :master, current_user: current_user) end def add_owner(user, current_user = nil) - add_user(user, Gitlab::Access::OWNER, current_user: current_user) + add_user(user, :owner, current_user: current_user) end def has_owner?(user) diff --git a/app/models/member.rb b/app/models/member.rb index 69406379948..38a278ea559 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -80,49 +80,70 @@ class Member < ActiveRecord::Base find_by(invite_token: invite_token) end - # This method is used to find users that have been entered into the "Add members" field. - # These can be the User objects directly, their IDs, their emails, or new emails to be invited. - def user_for_id(user_id) - return user_id if user_id.is_a?(User) - - user = User.find_by(id: user_id) - user ||= User.find_by(email: user_id) - user ||= user_id - user - end - - def add_user(members, user_id, access_level, current_user: nil, expires_at: nil) - user = user_for_id(user_id) + def add_user(source, user, access_level, current_user: nil, expires_at: nil) + user = retrieve_user(user) + access_level = retrieve_access_level(access_level) # `user` can be either a User object or an email to be invited - if user.is_a?(User) - member = members.find_or_initialize_by(user_id: user.id) + 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) + else + source.members.build(invite_email: user) + end + + return member unless can_update_member?(current_user, member) + + member.attributes = { + created_by: member.created_by || current_user, + access_level: access_level, + expires_at: expires_at + } + + if member.request? + ::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute else - member = members.build - member.invite_email = user + member.save end - if can_update_member?(current_user, member) || project_creator?(member, access_level) - member.created_by ||= current_user - member.access_level = access_level - member.expires_at = expires_at + member + end - member.save - end + def access_levels + Gitlab::Access.sym_options end private + # This method is used to find users that have been entered into the "Add members" field. + # These can be the User objects directly, their IDs, their emails, or new emails to be invited. + def retrieve_user(user) + return user if user.is_a?(User) + + User.find_by(id: user) || User.find_by(email: user) || user + end + + def retrieve_access_level(access_level) + access_levels.fetch(access_level) { access_level.to_i } + end + def can_update_member?(current_user, member) # There is no current user for bulk actions, in which case anything is allowed - !current_user || - current_user.can?(:update_group_member, member) || - current_user.can?(:update_project_member, member) + !current_user || current_user.can?(:"update_#{member.type.underscore}", member) end - def project_creator?(member, access_level) - member.new_record? && member.owner? && - access_level.to_i == ProjectMember::MASTER + def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil) + users.each do |user| + add_user( + source, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) + end end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 2f13d339c89..1b54a85d064 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -12,6 +12,22 @@ class GroupMember < Member Gitlab::Access.options_with_owner end + def self.access_levels + Gitlab::Access.sym_options_with_owner + end + + def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil) + self.transaction do + add_users_to_source( + group, + users, + access_level, + current_user: current_user, + expires_at: expires_at + ) + end + end + def group source end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index ec2d40eb11c..125f26369d7 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -34,36 +34,20 @@ class ProjectMember < Member # :master # ) # - def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil) - access_level = if roles_hash.has_key?(access) - roles_hash[access] - elsif roles_hash.values.include?(access.to_i) - access - else - raise "Non valid access" - end - - users = user_ids.map { |user_id| Member.user_for_id(user_id) } - - ProjectMember.transaction do + def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil) + self.transaction do project_ids.each do |project_id| project = Project.find(project_id) - users.each do |user| - Member.add_user( - project.project_members, - user, - access_level, - current_user: current_user, - expires_at: expires_at - ) - end + add_users_to_source( + project, + users, + access_level, + current_user: current_user, + expires_at: expires_at + ) end end - - true - rescue - false end def truncate_teams(project_ids) @@ -84,13 +68,15 @@ class ProjectMember < Member truncate_teams [project.id] end - def roles_hash - Gitlab::Access.sym_options - end - def access_level_roles Gitlab::Access.options end + + private + + def can_update_member?(current_user, member) + super || (member.owner? && member.new_record?) + end end def access_field diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2dcf7f89bfc..071dfe54ef9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -155,6 +155,20 @@ class MergeRequest < ActiveRecord::Base where("merge_requests.id IN (#{union.to_sql})") end + WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze + + def self.work_in_progress?(title) + !!(title =~ WIP_REGEX) + end + + def self.wipless_title(title) + title.sub(WIP_REGEX, "") + end + + def self.wip_title(title) + work_in_progress?(title) ? title : "WIP: #{title}" + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -389,14 +403,16 @@ class MergeRequest < ActiveRecord::Base @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end - WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze - def work_in_progress? - !!(title =~ WIP_REGEX) + self.class.work_in_progress?(title) end def wipless_title - self.title.sub(WIP_REGEX, "") + self.class.wipless_title(self.title) + end + + def wip_title + self.class.wip_title(self.title) end def mergeable?(skip_ci_check: false) @@ -507,9 +523,13 @@ class MergeRequest < ActiveRecord::Base # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires # running `ReferenceExtractor` on each of them separately. + # This optimization does not apply to issues from external sources. def cache_merge_request_closes_issues!(current_user = self.author) + return if project.has_external_issue_tracker? + transaction do self.merge_requests_closing_issues.delete_all + closes_issues(current_user).each do |issue| self.merge_requests_closing_issues.create!(issue: issue) end @@ -590,13 +610,11 @@ class MergeRequest < ActiveRecord::Base end def merge_commit_message - message = "Merge branch '#{source_branch}' into '#{target_branch}'" - message << "\n\n" - message << title.to_s - message << "\n\n" - message << description.to_s - message << "\n\n" - message << "See merge request !#{iid}" + message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" + message << "#{title}\n\n" + message << "#{description}\n\n" if description.present? + message << "See merge request #{to_reference}" + message end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 2bd7f198030..44c3cbb2c73 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -158,7 +158,7 @@ class Milestone < ActiveRecord::Base end def title=(value) - write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? + write_attribute(:title, sanitize_title(value)) if value.present? end # Sorts the issues for the given IDs. @@ -204,4 +204,8 @@ class Milestone < ActiveRecord::Base iid end end + + def sanitize_title(value) + CGI.unescape_html(Sanitize.clean(value.to_s)) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 7265cb55594..ecd742a17d5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -146,6 +146,7 @@ class Project < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true + delegate :add_user, to: :team # Validations validates :creator, presence: true, on: :create @@ -379,6 +380,7 @@ class Project < ActiveRecord::Base SELECT project_id, COUNT(*) AS amount FROM notes WHERE created_at >= #{sanitize(since)} + AND system IS FALSE GROUP BY project_id ) join_note_counts ON projects.id = join_note_counts.project_id" @@ -1016,10 +1018,6 @@ class Project < ActiveRecord::Base project_members.find_by(user_id: user) end - def add_user(user, access_level, current_user: nil, expires_at: nil) - team.add_user(user, access_level, current_user: current_user, expires_at: expires_at) - end - def default_branch @default_branch ||= repository.root_ref if repository.exists? end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 8c9534c3565..530f7d5a30e 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base FEATURES = %i(issues merge_requests wiki snippets builds) - belongs_to :project + # Default scopes force us to unscope here since a service may need to check + # permissions for a project in pending_delete + # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to + belongs_to :project, -> { unscope(where: :pending_delete) } default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 88e053ec192..cd87a79d0c6 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -11,7 +11,7 @@ class SlackService attr_reader :description def initialize(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb index 11fc691022b..b7615c96068 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -10,7 +10,7 @@ class SlackService attr_reader :title def initialize(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index 89ba51cb662..9e84e90f38c 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -10,7 +10,7 @@ class SlackService def initialize(params) params = HashWithIndifferentAccess.new(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb index f336d9e7691..160ca3ac115 100644 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ b/app/models/project_services/slack_service/wiki_page_message.rb @@ -9,7 +9,7 @@ class SlackService attr_reader :description def initialize(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_team.rb b/app/models/project_team.rb index d9ce5088903..79d041d2775 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -33,18 +33,24 @@ class ProjectTeam member end - def add_users(users, access, current_user: nil, expires_at: nil) + def add_users(users, access_level, current_user: nil, expires_at: nil) ProjectMember.add_users_to_projects( [project.id], users, - access, + access_level, current_user: current_user, expires_at: expires_at ) end - def add_user(user, access, current_user: nil, expires_at: nil) - add_users([user], access, current_user: current_user, expires_at: expires_at) + def add_user(user, access_level, current_user: nil, expires_at: nil) + ProjectMember.add_user( + project, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) end # Remove all users from project team diff --git a/app/models/repository.rb b/app/models/repository.rb index 51557228ab9..eb574555df6 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -997,6 +997,10 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end + def create_ref(ref, ref_path) + fetch_ref(path_to_repo, ref, ref_path) + end + def update_branch_with_hooks(current_user, branch) update_autocrlf_option diff --git a/app/models/service.rb b/app/models/service.rb index 80de7175565..66c804f2b06 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -136,6 +136,7 @@ class Service < ActiveRecord::Base end def #{arg}=(value) + self.properties ||= {} updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? self.properties['#{arg}'] = value end diff --git a/app/models/user.rb b/app/models/user.rb index 6996740eebd..508efd85050 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -279,6 +279,11 @@ class User < ActiveRecord::Base find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) end + # Returns a user for the given SSH key. + def find_by_ssh_key_id(key_id) + find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) + end + def build_user(attrs = {}) User.new(attrs) end @@ -827,6 +832,22 @@ class User < ActiveRecord::Base todos_pending_count(force: true) end + # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth + # flow means we don't call that automatically (and can't conveniently do so). + # + # See: + # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92> + # + def increment_failed_attempts! + self.failed_attempts ||= 0 + self.failed_attempts += 1 + if attempts_exceeded? + lock_access! unless access_locked? + else + save(validate: false) + end + end + private def projects_union(min_access_level = nil) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 38ac6631228..8ea88da8a53 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -7,10 +7,10 @@ module Auth def execute(authentication_abilities:) @authentication_abilities = authentication_abilities - return error('not found', 404) unless registry.enabled + return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled unless current_user || project - return error('forbidden', 403) unless scope + return error('DENIED', status: 403, message: 'access forbidden') unless scope end { token: authorized_token(scope).encoded } @@ -111,5 +111,12 @@ module Auth @authentication_abilities.include?(:create_container_image) && can?(current_user, :create_container_image, requested_project) end + + def error(code, status:, message: '') + { + errors: [{ code: code, message: message }], + http_status: status + } + end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index fbce46769f7..57d521f2fea 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -50,6 +50,7 @@ class IssuableBaseService < BaseService params.delete(:remove_label_ids) params.delete(:label_ids) params.delete(:assignee_id) + params.delete(:due_date) end end diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb new file mode 100644 index 00000000000..416aee2ab51 --- /dev/null +++ b/app/services/members/approve_access_request_service.rb @@ -0,0 +1,31 @@ +module Members + class ApproveAccessRequestService < BaseService + include MembersHelper + + attr_accessor :source + + def initialize(source, current_user, params = {}) + @source = source + @current_user = current_user + @params = params + end + + def execute + condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } + access_requester = source.requesters.find_by!(condition) + + raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester) + + access_requester.access_level = params[:access_level] if params[:access_level] + access_requester.accept_request + + access_requester + end + + private + + def can_update_access_requester?(access_requester) + access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester) + end + end +end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index ca9db59cac7..b7a244c2029 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -14,6 +14,8 @@ module Members if member.request? && member.user != user notification_service.decline_access_request(member) end + + member end end end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 9a2bf82ef51..431da8372c9 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -1,17 +1,42 @@ module Members class DestroyService < BaseService - attr_accessor :member, :current_user + include MembersHelper - def initialize(member, current_user) - @member = member + attr_accessor :source + + ALLOWED_SCOPES = %i[members requesters all] + + def initialize(source, current_user, params = {}) + @source = source @current_user = current_user + @params = params end - def execute - unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) - raise Gitlab::Access::AccessDeniedError - end + def execute(scope = :members) + raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope) + + member = find_member!(scope) + + raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member) + AuthorizedDestroyService.new(member, current_user).execute end + + private + + def find_member!(scope) + condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } + case scope + when :all + source.members.find_by(condition) || + source.requesters.find_by!(condition) + else + source.public_send(scope).find_by!(condition) + end + end + + def can_destroy_member?(member) + member && can?(current_user, action_member_permission(:destroy, member), member) + end end end diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb new file mode 100644 index 00000000000..2614153d900 --- /dev/null +++ b/app/services/members/request_access_service.rb @@ -0,0 +1,25 @@ +module Members + class RequestAccessService < BaseService + attr_accessor :source + + def initialize(source, current_user) + @source = source + @current_user = current_user + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can_request_access?(source) + + source.members.create( + access_level: Gitlab::Access::DEVELOPER, + user: current_user, + requested_at: Time.now.utc) + end + + private + + def can_request_access?(source) + source && can?(current_user, :request_access, source) + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index ba424b09463..d0d155b7ee1 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -5,16 +5,17 @@ module MergeRequests end def create_title_change_note(issuable, old_title) - removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress? - added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress? + removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress? + added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress? + changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title if removed_wip SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user) elsif added_wip SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user) - else - super end + + super if changed_title end def hook_data(merge_request, action, oldrev = nil) diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 8437d9b8b43..e8fb1b59752 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -7,6 +7,7 @@ module MergeRequests class PostMergeService < MergeRequests::BaseService def execute(merge_request) close_issues(merge_request) + todo_service.merge_merge_request(merge_request, current_user) merge_request.mark_as_merged create_merge_event(merge_request, current_user) create_note(merge_request) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index f14f9e4b327..9dbec49d163 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -16,7 +16,7 @@ module MergeRequests end merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) - + handle_wip_event(merge_request) update(merge_request) end @@ -81,5 +81,18 @@ module MergeRequests def after_update(issuable) issuable.cache_merge_request_closes_issues!(current_user) end + + private + + def handle_wip_event(merge_request) + if wip_event = params.delete(:wip_event) + # We update the title that is provided in the params or we use the mr title + title = params[:title] || merge_request.title + params[:title] = case wip_event + when 'wip' then MergeRequest.wip_title(title) + when 'unwip' then MergeRequest.wipless_title(title) + end + end + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 6139ed56e25..de8049b8e2e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -134,7 +134,8 @@ class NotificationService merge_request, merge_request.target_project, current_user, - :merged_merge_request_email + :merged_merge_request_email, + skip_current_user: !merge_request.merge_when_build_succeeds? ) end @@ -514,9 +515,16 @@ class NotificationService end end - def close_resource_email(target, project, current_user, method) + def close_resource_email(target, project, current_user, method, skip_current_user: true) action = method == :merged_merge_request_email ? "merge" : "close" - recipients = build_recipients(target, project, current_user, action: action) + + recipients = build_recipients( + target, + project, + current_user, + action: action, + skip_current_user: skip_current_user + ) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, current_user.id).deliver_later @@ -557,7 +565,7 @@ class NotificationService end end - def build_recipients(target, project, current_user, action: nil, previous_assignee: nil) + def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true) custom_action = build_custom_key(action, target) recipients = target.participants(current_user) @@ -586,7 +594,8 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) + recipients.delete(current_user) if skip_current_user + recipients.uniq end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 696fe3efe8f..15d7918e7fd 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -7,6 +7,8 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) import_data = params.delete(:import_data) + @skip_wiki = params.delete(:skip_wiki) + @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level @@ -92,7 +94,7 @@ module Projects log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") unless @project.gitlab_project_import? - @project.create_wiki if @project.feature_available?(:wiki, current_user) + @project.create_wiki unless skip_wiki? @project.build_missing_services @project.create_labels @@ -106,6 +108,10 @@ module Projects end end + def skip_wiki? + !@project.feature_available?(:wiki, current_user) || @skip_wiki + end + def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index cdad0426b02..e466ffa60eb 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -44,6 +44,11 @@ module Projects begin gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) rescue => e + # Expire cache to prevent scenarios such as: + # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true + # 2. Retried import, repo is broken or not imported but +exists?+ still returns true + project.repository.before_import if project.repository_exists? + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 9ac1124abc1..1725a30fae5 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -195,7 +195,7 @@ module SlashCommands params '<in 2 days | this Friday | December 31st>' condition do issuable.respond_to?(:due_date) && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + current_user.can?(:"admin_#{issuable.to_ability_name}", project) end command :due do |due_date_param| due_date = Chronic.parse(due_date_param).try(:to_date) @@ -208,12 +208,24 @@ module SlashCommands issuable.persisted? && issuable.respond_to?(:due_date) && issuable.due_date? && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + current_user.can?(:"admin_#{issuable.to_ability_name}", project) end command :remove_due_date do @updates[:due_date] = nil end + desc do + "Toggle the Work In Progress status" + end + condition do + issuable.persisted? && + issuable.respond_to?(:work_in_progress?) && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + command :wip do + @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' + end + # This is a dummy command, so that it appears in the autocomplete commands desc 'CC' params '@user' diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 1fb72cf89e9..a2bfa422c9d 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -72,7 +72,7 @@ class SystemHooksService return 'user_add_to_group' if event == :create return 'user_remove_from_group' if event == :destroy else - "#{model.class.name.downcase}_#{event.to_s}" + "#{model.class.name.downcase}_#{event}" end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 0c8446e7c3d..1ce66d50368 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -24,6 +24,7 @@ module SystemNoteService body = "Added #{commits_text}:\n\n" body << existing_commit_summary(noteable, existing_commits, oldrev) body << new_commit_summary(new_commits).join("\n") + body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -245,7 +246,7 @@ module SystemNoteService 'deleted' end - body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize + body = "#{verb} #{branch_type} branch `#{branch}`".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -254,8 +255,7 @@ module SystemNoteService # # "Started branch `201-issue-branch-button`" def new_issue_branch(issue, project, author, branch) - h = Gitlab::Routing.url_helpers - link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) + link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "Started branch [`#{branch}`](#{link})" create_note(noteable: issue, project: project, author: author, note: body) @@ -347,7 +347,7 @@ module SystemNoteService notes = notes.where(noteable_id: noteable.id) end - notes_for_mentioner(mentioner, noteable, notes).count > 0 + notes_for_mentioner(mentioner, noteable, notes).exists? end # Build an Array of lines detailing each commit added in a merge request @@ -466,4 +466,20 @@ module SystemNoteService def escape_html(text) Rack::Utils.escape_html(text) end + + def url_helpers + @url_helpers ||= Gitlab::Routing.url_helpers + end + + def diff_comparison_url(merge_request, project, oldrev) + diff_id = merge_request.merge_request_diff.id + + url_helpers.diffs_namespace_project_merge_request_url( + project.namespace, + project, + merge_request.iid, + diff_id: diff_id, + start_sha: oldrev + ) + end end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 7a35958cc5f..4dc3b2ab9a0 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -5,7 +5,8 @@ # Values are checked for formatting and exclusion from a list of reserved path # names. class NamespaceValidator < ActiveModel::EachValidator - RESERVED = %w( + RESERVED = %w[ + .well-known admin all assets @@ -31,7 +32,7 @@ class NamespaceValidator < ActiveModel::EachValidator u unsubscribes users - ).freeze + ].freeze def validate_each(record, attribute, value) unless value =~ Gitlab::Regex.namespace_regex diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml index 107fc25244a..b3530915068 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/background_jobs/_head.html.haml @@ -1,24 +1,25 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 6b157abf842..f952d2e9aa1 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -18,11 +18,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.color_field :color, class: "form-control" + = f.text_field :color, class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.color_field :font, class: "form-control" + = f.text_field :font, class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index c91ab4cb946..ec40391a3e3 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -1,28 +1,29 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Overview - = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do - %span - Builds - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Overview + = nav_link(controller: [:admin, :projects]) do + = link_to admin_namespaces_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'builds#index' do + = link_to admin_builds_path, title: 'Builds' do + %span + Builds + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e6687f43816..90798c47d97 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -63,6 +63,11 @@ Reply by email %span.light.pull-right = boolean_to_icon Gitlab::IncomingEmail.enabled? + %p + Container Registry + %span.light.pull-right + = boolean_to_icon Gitlab.config.registry.enabled + .col-md-4 %h4 Components diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 602cfa9b6fc..d5e6bede36a 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -14,7 +14,7 @@ .col-sm-10 .input-group .input-group-addon.label-color-preview - = f.color_field :color, class: "form-control" + = f.text_field :color, class: "form-control" .help-block Choose any color. %br diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index a53876d6757..b760b42fde0 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -5,8 +5,10 @@ %p.prepend-top-default %span - To register a new runner you should enter the following registration token. - With this token the runner will request a unique runner token and use that for future communication. + To register a new Runner you should enter the following registration + token. + With this token the Runner will request a unique Runner token and use + that for future communication. %br Registration token is %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} @@ -24,27 +26,27 @@ .bs-callout %p - A 'runner' is a process which runs a build. - You can setup as many runners as you need. + A 'Runner' is a process which runs a build. + You can setup as many Runners as you need. %br - Runners can be placed on separate users, servers, and even on your local machine. + Runners can be placed on separate users, servers, even on your local machine. %br %div - %span Each runner can be in one of the following states: + %span Each Runner can be in one of the following states: %ul %li %span.label.label-success shared - \- run builds from all unassigned projects + \- Runner runs builds from all unassigned projects %li %span.label.label-info specific - \- run builds from assigned projects + \- Runner runs builds from assigned projects %li %span.label.label-warning locked - \- runner cannot be assigned to other projects + \- Runner cannot be assigned to other projects %li %span.label.label-danger paused - \- runner will not receive any new builds + \- Runner will not receive any new builds .append-bottom-20.clearfix .pull-left diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 61abfc6ecbe..a5e82e55cc1 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -11,14 +11,14 @@ - if @runner.shared? .bs-callout.bs-callout-success - %h4 This runner will process builds from ALL UNASSIGNED projects + %h4 This Runner will process builds from ALL UNASSIGNED projects %p - If you want runners to build only specific projects, enable them in the table below. + If you want Runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. - else .bs-callout.bs-callout-info - %h4 This runner will process builds only from ASSIGNED projects - %p You can't make this a shared runner. + %h4 This Runner will process builds only from ASSIGNED projects + %p You can't make this a shared Runner. %hr .append-bottom-20 @@ -26,7 +26,7 @@ .row .col-md-6 - %h4 Restrict projects for this runner + %h4 Restrict projects for this Runner - if @runner.projects.any? %table.table.assigned-projects %thead @@ -70,7 +70,7 @@ = paginate @projects .col-md-6 - %h4 Recent builds served by this runner + %h4 Recent builds served by this Runner %table.table.builds.runner-builds %thead %tr diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index 1545c00af45..61c7cce20b2 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -20,13 +20,16 @@ %br %b Tag list: - = build[:tags] + = build[:tag_list].to_a.join(", ") %br %b Refs only: - = build[:only] && build[:only].join(", ") + = @jobs[build[:name].to_sym][:only].to_a.join(", ") %br %b Refs except: - = build[:except] && build[:except].join(", ") + = @jobs[build[:name].to_sym][:except].to_a.join(", ") + %br + %b Environment: + = build[:environment] %br %b When: = build[:when] diff --git a/app/views/dashboard/groups/_empty_state.html.haml b/app/views/dashboard/groups/_empty_state.html.haml new file mode 100644 index 00000000000..f5222fe631e --- /dev/null +++ b/app/views/dashboard/groups/_empty_state.html.haml @@ -0,0 +1,7 @@ +.groups-empty-state + = custom_icon("icon_empty_groups") + + .text-content + %h4 A group is a collection of several projects. + %p If you organize your projects under a group, it works like a folder. + %p You can manage your group member’s permissions and access to each project in the group. diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index caca91af536..1a679c51774 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,9 +2,12 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -%ul.content-list - - @group_members.each do |group_member| - - group = group_member.group - = render 'shared/groups/group', group: group, group_member: group_member +- if @group_members.empty? + = render 'empty_state' +- else + %ul.content-list + - @group_members.each do |group_member| + - group = group_member.group + = render 'shared/groups/group', group: group, group_member: group_member -= paginate @group_members, theme: 'gitlab' + = paginate @group_members, theme: 'gitlab' diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index d4e7862981c..b2af438ea57 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -4,10 +4,10 @@ = render 'dashboard/snippets_head' .nav-block - .controls - = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do + .controls.hidden-xs + = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do = icon('plus') - New Snippet + New snippet .nav-links.snippet-scope-menu %li{ class: ("active" unless params[:scope]) } @@ -34,5 +34,9 @@ %span.badge = current_user.snippets.are_public.count -= render 'snippets/snippets' + .visible-xs + = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do + = icon('plus') + New snippet += render 'snippets/snippets' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index b40395c74de..cc077fad32a 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -19,6 +19,7 @@ (removed) · #{time_ago_with_tooltip(todo.created_at)} + = todo_due_date(todo) .todo-body .todo-note diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml index 7a8767ddba0..f0b61e0f7de 100644 --- a/app/views/discussions/_resolve_all.html.haml +++ b/app/views/discussions/_resolve_all.html.haml @@ -1,11 +1,10 @@ - if discussion.for_merge_request? - %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'", - ":project-path" => "'#{discussion.project.path}'", + %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'", ":discussion-id" => "'#{discussion.id}'", ":merge-request-id" => discussion.noteable.iid, ":can-resolve" => discussion.can_resolve?(current_user), "inline-template" => true } .btn-group{ role: "group", "v-if" => "showButton" } - %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading" } + %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" } = icon("spinner spin", "v-show" => "loading") {{ buttonText }} diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index bfa95ce79a7..9f02a8d2ed9 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -6,4 +6,4 @@ = form_tag path do %input{:name => "_method", :type => "hidden", :value => "delete"}/ - = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm' + = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm' diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index cd485da5104..132bbe26fe0 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -8,7 +8,7 @@ - else Any %b.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(visibility_level: nil) do Any @@ -28,7 +28,7 @@ - else Any %b.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(tag: nil) do Any diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 6306fe6d0bf..7def9eacdc9 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -8,9 +8,8 @@ .row-content-block - if current_user - .pull-right - = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - New Snippet + = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do + New snippet .oneline Public snippets created by you and other users are listed here diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index 3612f1ce5c6..baa8036de10 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -1,8 +1,10 @@ .flash-container.flash-container-page - if alert .flash-alert - = alert + %div{ class: (container_class) } + %span= alert - elsif notice .flash-notice - = notice + %div{ class: (container_class) } + %span= notice diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 4f7839a881f..8aefdcb3d9b 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,10 +1,11 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .sidebar-wrapper.nicescroll .sidebar-action-buttons - = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do + .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" } %span.sr-only Toggle navigation = icon('bars') - = link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do + + %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } } %span.sr-only Toggle navigation pinning = icon('fw thumb-tack') @@ -20,6 +21,7 @@ .container-fluid = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } + = yield :sub_nav = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index f7580f00159..d7386105b7d 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -2,15 +2,18 @@ - label = 'This group' - if controller.controller_path =~ /^projects/ && @project.persisted? - label = 'This project' - +- if @group && @group.persisted? && @group.path + - 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?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - if label.present? .location-badge= label .search-input-wrap - .dropdown{ data: {url: search_autocomplete_path } } - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } + .dropdown{ data: { url: search_autocomplete_path } } + = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } .dropdown-menu.dropdown-select = dropdown_content do %ul @@ -21,8 +24,9 @@ %i.search-icon %i.clear-icon.js-clear-input - = hidden_field_tag :group_id, @group.try(:id) - = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' + = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs + + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: project_data_attrs - if @project && @project.persisted? - if current_controller?(:issues) @@ -36,31 +40,6 @@ - else = hidden_field_tag :search_code, true - :javascript - gl.projectOptions = gl.projectOptions || {}; - gl.projectOptions["#{j(@project.path)}"] = { - issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}", - mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}", - name: "#{j(@project.name)}" - }; - - - if @group && @group.persisted? && @group.path - :javascript - gl.groupOptions = gl.groupOptions || {}; - gl.groupOptions["#{j(@group.path)}"] = { - name: "#{j(@group.name)}", - issuesPath: "#{issues_group_path(j(@group.path))}", - mrPath: "#{merge_requests_group_path(j(@group.path))}" - }; - - - :javascript - gl.dashboardOptions = { - issuesPath: "#{issues_dashboard_url}", - mrPath: "#{merge_requests_dashboard_url}" - }; - - - if @snippet || @snippets = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 94c53882623..237280872f1 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,5 +1,5 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } + %div{ class: "container-fluid" } .header-content %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %span.sr-only Toggle navigation diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 4433cab7782..8966dd3fd86 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>') // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { - $('.content-wrapper').find('.container-fluid').removeClass('container-limited') + $('.content-wrapper .container-fluid').removeClass('container-limited') } else { - $('.content-wrapper').find('.container-fluid').addClass('container-limited') + $('.content-wrapper .container-fluid').addClass('container-limited') } // Re-enable the "Save" button diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index d9fa74fad90..578af9fe98d 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -87,6 +87,9 @@ = f.label :location, 'Location', class: "label-light" = f.text_field :location, class: "form-control" .form-group + = f.label :organization, 'Organization', class: "label-light" + = f.text_field :organization, class: "form-control" + .form-group = f.label :bio, class: "label-light" = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250 %span.help-block Tell us about yourself in fewer than 250 characters. diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index ac50ce83f6a..d011e51e696 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,13 +1,16 @@ -.nav-block.activity-filter-block - - if current_user - .controls - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do - %i.fa.fa-rss +- @no_container = true - = render 'shared/event_filter' +%div{ class: container_class } + .nav-block.activity-filter-block + - if current_user + .controls + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do + = icon('rss') -.content_list.project-activity{:"data-href" => activity_project_path(@project)} -= spinner + = render 'shared/event_filter' + + .content_list.project-activity{:"data-href" => activity_project_path(@project)} + = spinner :javascript var activity = new Activities(); diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 3c6b931f41a..1c3bccccb5c 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -1,6 +1,6 @@ - if event = last_push_event - if show_last_push_widget?(event) - .row-content-block.top-block.clear-block.hidden-xs + .row-content-block.top-block.hidden-xs.white %div{ class: container_class } .event-last-push .event-last-push-text diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 0237e152b54..d4f59764a70 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -21,6 +21,13 @@ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: '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: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + = button_tag class: 'soft-wrap-toggle btn', type: 'button' do + %span.no-wrap + = custom_icon('icon_no_wrap') + No wrap + %span.soft-wrap + = custom_icon('icon_soft_wrap') + Soft wrap .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 0aa3092baa2..f5344091cae 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -8,7 +8,7 @@ %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } = icon('angle-double-right') - if @build.coverage - .block.block-first + .block.coverage .title Test coverage %p.build-detail-row @@ -95,7 +95,7 @@ - @build.trigger_request.variables.each do |key, value| .hide.js-build - .js-build-variable= key + .js-build-variable= key .js-build-value= value .block @@ -128,7 +128,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{class: ('active' if build == @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('check') + = icon('right-arrow') = ci_icon_for_status(build.status) %span - if build.name diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index c2bcfb773a6..f3747ba2a21 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -1,7 +1,7 @@ - admin = local_assigns.fetch(:admin, false) - if builds.blank? - %li + %div .nothing-here-block No builds to show - else .table-holder diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 5c60b7a7364..06070f12bbd 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 - %ul.content-list.builds-content-list + %div.content-list.builds-content-list = render "table", builds: @builds, project: @project diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 24de020917a..9089586a89d 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,9 +1,9 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} + %span{class: 'hidden-xs hidden-sm'} .dropdown.inline %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') - %span.caret + = icon("caret-down") %span.sr-only Select Archive Format %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index ca907077c2b..6cd9b98a706 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,7 +1,8 @@ - if current_user - .btn-group + .dropdown.inline.project-dropdown %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') + = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - can_create_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 75192c48188..9248adfde80 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,45 +13,44 @@ - else = ci_status_with_icon(build.status) - %td - .branch-commit - - if can?(current_user, :read_build, build) - = link_to namespace_project_build_url(build.project.namespace, build.project, build) do - %span.build-link ##{build.id} - - else + %td.branch-commit + - if can?(current_user, :read_build, build) + = link_to namespace_project_build_url(build.project.namespace, build.project, build) do %span.build-link ##{build.id} + - else + %span.build-link ##{build.id} - - if ref - - if build.ref - .icon-container - = build.tag? ? icon('tag') : icon('code-fork') - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - - else - .light none + - if ref + - if build.ref .icon-container - = custom_icon("icon_commit") + = build.tag? ? icon('tag') : icon('code-fork') + = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" + - else + .light none + .icon-container + = custom_icon("icon_commit") - - if commit_sha - = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + - if commit_sha + = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" - - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - - if retried - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + - if build.stuck? + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - .label-container - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail - - if retried - %span.label.label-warning retried - - if build.manual? - %span.label.label-info manual + .label-container + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + - if retried + %span.label.label-warning retried + - if build.manual? + %span.label.label-info manual - if admin %td diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 6391c67021b..b87c7a485df 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,4 +1,7 @@ - status = pipeline.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) do @@ -6,33 +9,32 @@ = ci_icon_for_status(status) - else = ci_status_with_icon(status) - %td - .branch-commit - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - %span ##{pipeline.id} - - if pipeline.ref - - unless defined?(hide_branch) && hide_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" + %td.branch-commit + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do + %span ##{pipeline.id} + - if pipeline.ref && show_branch + .icon-container + = pipeline.tag? ? icon('tag') : icon('code-fork') + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" + - if show_commit .icon-container = custom_icon("icon_commit") = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" - - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - - if pipeline.triggered? - %span.label.label-primary triggered - - if pipeline.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid - - if pipeline.builds.any?(&:stuck?) - %span.label.label-warning stuck + - if pipeline.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + - if pipeline.triggered? + %span.label.label-primary triggered + - if pipeline.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid + - if pipeline.builds.any?(&:stuck?) + %span.label.label-warning stuck - %p.commit-title - - if commit = pipeline.commit - = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch + %p.commit-title + - if commit = pipeline.commit + = author_avatar(commit, size: 20) + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch - stages_status = pipeline.statuses.relevant.latest.stages_status @@ -55,8 +57,8 @@ = icon("calendar") #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)} - %td.pipeline-actions - .controls.hidden-xs.pull-right + %td.pipeline-actions.hidden-xs + .controls.pull-right - artifacts = pipeline.builds.latest.with_artifacts_not_expired - actions = pipeline.manual_actions - if artifacts.present? || actions.any? diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index a508382578a..b7087749428 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,2 +1,2 @@ -- @pipelines.each do |pipeline| +- @ci_pipelines.each do |pipeline| = render "pipeline", pipeline: pipeline, pipeline_details: true diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 935433306ea..cbfd99ca448 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -3,6 +3,11 @@ = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes %span.badge= @diffs.size + - if can?(current_user, :read_pipeline, @project) + = nav_link(path: 'commit#pipelines') do + = 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 diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index f41a11a056d..998812793a2 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -7,14 +7,8 @@ %table.table.builds %tbody %th Status - %th Commit - - pipelines.stages.each do |stage| - %th.stage - - if stage.titleize.length > 12 - %span.has-tooltip{ title: "#{stage.titleize}" } - = stage.titleize - - else - = stage.titleize + %th Pipeline + %th Stages %th %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true + = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, show_commit: false diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml new file mode 100644 index 00000000000..d85d6729a81 --- /dev/null +++ b/app/views/projects/commit/pipelines.html.haml @@ -0,0 +1,7 @@ +- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits" + +.prepend-top-default + = render "commit_box" + += render "ci_menu" += render "pipelines_list", pipelines: @ci_pipelines diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 4d1ee1c5318..80763ce67ca 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,27 +1,28 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project) do - Files += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files - = nav_link(controller: [:commit, :commits]) do - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - Commits + = nav_link(controller: [:commit, :commits]) do + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + Commits - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Network + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network - = nav_link(controller: :compare) do - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + Compare - = nav_link(html_options: {class: branches_tab_class}) do - = link_to namespace_project_branches_path(@project.namespace, @project) do - Branches + = nav_link(html_options: {class: branches_tab_class}) do + = link_to namespace_project_branches_path(@project.namespace, @project) do + Branches - = nav_link(controller: [:tags, :releases]) do - = link_to namespace_project_tags_path(@project.namespace, @project) do - Tags + = nav_link(controller: [:tags, :releases]) do + = link_to namespace_project_tags_path(@project.namespace, @project) do + Tags diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 9a44ba94970..876c8002627 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -5,7 +5,8 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") -= render "head" += content_for :sub_nav do + = render "head" %div{ class: container_class } .row-content-block.second-block.content-component-block diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index d79336f5a60..76b68c544aa 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,17 +1,22 @@ = form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do .clearfix - if params[:to] && params[:from] - = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} - .form-group.dropdown.compare-form-group.js-compare-from-dropdown + .compare-switch-container + = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', 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 - = text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence } + = 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' = render "ref_dropdown" - = "..." - .form-group.dropdown.compare-form-group.js-compare-to-dropdown + .compare-ellipsis ... + .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to - = text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence } + = 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' = render "ref_dropdown" = button_tag "Compare", class: "btn btn-create commits-compare-btn" diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml index c604c6d0135..27d928c87a0 100644 --- a/app/views/projects/compare/_ref_dropdown.html.haml +++ b/app/views/projects/compare/_ref_dropdown.html.haml @@ -1,4 +1,5 @@ .dropdown-menu.dropdown-menu-selectable = dropdown_title "Select branch/tag" + = dropdown_filter "Filter by branch/tag" = dropdown_content = dropdown_loading diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index d37961c4e40..779c8ea0104 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -11,7 +11,9 @@ - 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)) .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } - This diff is collapsed. Click to expand it. + This diff is collapsed. + %a.click-to-expand + Click to expand it. - elsif diff_file.diff_lines.length > 0 - if diff_view == :parallel = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 62aff36aadd..576e7ef021a 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,7 +1,5 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - diff_files = diffs.diff_files -- if diff_view == :parallel - - fluid_layout true .content-block.oneline-block.files-changed .inline-parallel-buttons diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 1a51ccd4c7d..d07de45fdde 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -5,7 +5,7 @@ - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this files", disabled: @diff_notes_disabled do + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 95a2772fd0b..a6a2e5690b5 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -1,3 +1,4 @@ +%i.fa.diff-toggle-caret - if defined?(blob) && blob && diff_file.submodule? %span = icon('archive fw') diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 636beb73ec2..7a39064adc5 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -23,6 +23,8 @@ or a = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link' to this project. + %p + You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected. - if can?(current_user, :push_code, @project) %div{ class: container_class } diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 082e2cb4d8c..1a62a6a809c 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,18 +1,19 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } - - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/chart.js') - = page_specific_javascript_tag('graphs/graphs_bundle.js') - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.feature_available?(:builds, current_user) - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/chart.js') + = page_specific_javascript_tag('graphs/graphs_bundle.js') + = nav_link(action: :show) do + = link_to 'Contributors', namespace_project_graph_path + = nav_link(action: :commits) do + = link_to 'Commits', commits_namespace_project_graph_path + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path + - if @project.feature_available?(:builds, current_user) + = nav_link(action: :ci) do + = link_to ci_namespace_project_graph_path do + Continuous Integration diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml index ca700cb3a3b..4c5dd9b88bf 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/index.html.haml @@ -8,10 +8,10 @@ .col-lg-9 %h5.prepend-top-0 Set a group to share - = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post do + = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do .form-group = label_tag :link_group_id, "Group", class: "label-light" - = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path)) + = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true) .form-group = label_tag :link_group_access, "Max access level", class: "label-light" .select-wrapper diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index f88b33018d0..509b01c548a 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,32 +1,33 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) - = nav_link(controller: :issues) do - = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do - %span - Issues += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) + = nav_link(controller: :issues) do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do + %span + Issues - = nav_link(controller: :boards) do - = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do - %span - Board + = nav_link(controller: :boards) do + = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do + %span + Board - - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do - %span - Merge Requests + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + Merge Requests - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - %span - Labels + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - %span - Milestones
\ No newline at end of file + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 8da9f2100e9..cc57cfdb342 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -3,7 +3,8 @@ - page_title "Issues" - new_issue_email = @project.new_issue_address(current_user) -= render "projects/issues/head" += content_for :sub_nav do + = render "projects/issues/head" = content_for :meta_tags do - if current_user diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index aa143e54ffe..6ab6ae50389 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -14,7 +14,7 @@ .col-sm-10 .input-group .input-group-addon.label-color-preview - = f.color_field :color, class: "form-control" + = f.text_field :color, class: "form-control" .help-block Choose any color. %br diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 3900b4f6f17..cfb44bd206c 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -5,7 +5,7 @@ - if @merge_request.reopenable? = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" } - %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } } + %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } {{ buttonText }} #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index d03ff9ec7e8..9f34ca9ff4e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -4,9 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') -- if diff_view == :parallel - - fluid_layout true - .merge-request{'data-url' => merge_request_path(@merge_request)} = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 49819519759..904452fcc4f 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -58,11 +58,11 @@ .monospace #{short_sha(merge_request_diff.head_commit_sha)} %small = time_ago_with_tooltip(merge_request_diff.created_at) - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do - %strong - #{@merge_request.target_branch} (base) - .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do + %strong + #{@merge_request.target_branch} (base) + .monospace #{short_sha(@merge_request_diff.base_commit_sha)} - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 44e645a7e81..b5f5e11d4c3 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -4,14 +4,15 @@ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span - CI build + Pipeline + = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index fda0592dd41..cc8cb134fb8 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -72,7 +72,7 @@ = link_to "#", class: 'btn js-toggle-button import_git' do = icon('git', text: 'Repo by URL') %div{ class: 'import_gitlab_project' } - - if gitlab_project_import_enabled? && current_user.is_admin? + - 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/_note.html.haml b/app/views/projects/notes/_note.html.haml index 9ec17cf6e76..788be4a0047 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -24,14 +24,12 @@ - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) - - %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'", - ":project-path" => "'#{note.project.path}'", - ":discussion-id" => "'#{note.discussion_id}'", + %resolve-btn{ "project-path" => "#{project_path(note.project)}", + "discussion-id" => "#{note.discussion_id}", ":note-id" => note.id, ":resolved" => note.resolved?, ":can-resolve" => can_resolve, - ":resolved-by" => "'#{note.resolved_by.try(:name)}'", + "resolved-by" => "#{note.resolved_by.try(:name)}", "v-show" => "#{can_resolve || note.resolved?}", "inline-template" => true, "v-ref:note_#{note.id}" => true } diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 5f571499e80..7d421c0e740 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,27 +1,28 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - %span - Builds + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds - - if project_nav_tab? :environments - = nav_link(controller: %w(environments)) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do - %span - Environments + - if project_nav_tab? :environments + = nav_link(controller: %w(environments)) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(controller: %w(cycle_analytics)) do - = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do - %span - Cycle Analytics + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(controller: %w(cycle_analytics)) do + = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do + %span + Cycle Analytics diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index faf28db68d1..2d1df095bfa 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -36,20 +36,20 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - %ul.content-list.pipelines + %div.content-list.pipelines - stages = @pipelines.stages - if @pipelines.blank? - %li + %div .nothing-here-block No pipelines to show - else .table-holder %table.table.builds - %tbody - %th Status - %th Commit - %th Stages - %th - %th + %thead + %th.col-xs-1.col-sm-1 Status + %th.col-xs-2.col-sm-4 Pipeline + %th.col-xs-2.col-sm-2 Stages + %th.col-xs-2.col-sm-2 + %th.hidden-xs.col-sm-3 = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index c45a9d4f81f..33a9a96183c 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -5,7 +5,7 @@ .col-sm-10 .checkbox = f.check_box :active - %span.light Paused runners don't accept new builds + %span.light Paused Runners don't accept new builds .form-group = label :run_untagged, 'Run untagged jobs', class: 'control-label' .col-sm-10 @@ -33,6 +33,6 @@ Tags .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control' - .help-block You can setup jobs to only use runners with specific tags + .help-block You can setup jobs to only use Runners with specific tags .form-actions = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 85225857758..6e58e5a0c78 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -15,7 +15,7 @@ .pull-right - if @project_runners.include?(runner) - if runner.belongs_to_one_project? - = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else - runner_project = @project.runner_projects.find_by(runner_id: runner) = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 9fa4127c948..752b9e060d5 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,24 +1,26 @@ -%h3 Shared runners +%h3 Shared Runners .bs-callout.bs-callout-warning.shared-runners-description - if shared_runners_text.present? = markdown(shared_runners_text, pipeline: 'plain_markdown') - else - Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com). + GitLab Shared Runners execute code of different projects on the same Runner + unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is + on GitLab.com). %hr - if @project.shared_runners_enabled? = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do - Disable shared runners + Disable shared Runners - else = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do - Enable shared runners + Enable shared Runners for this project - if @shared_runners_count.zero? - This GitLab server does not provide any shared runners yet. - Please use specific runners or ask the administrator to create one. + This GitLab server does not provide any shared Runners yet. + Please use the specific Runners or ask your administrator to create one. - else - %h4.underlined-title Available shared runners - #{@shared_runners_count} + %h4.underlined-title Available shared Runners : #{@shared_runners_count} %ul.bordered-list.available-shared-runners = render partial: 'runner', collection: @shared_runners, as: :runner - if @shared_runners_count > 10 diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index d469dda5b81..858af78f7bf 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -1,20 +1,20 @@ -%h3 Specific runners +%h3 Specific Runners .bs-callout.help-callout - %h4 How to setup a new project specific runner + %h4 How to setup a specific Runner for a new project %ol %li - Install GitLab Runner software. - Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it + Install a Runner compatible with GitLab CI + (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it). %li - Specify the following URL during runner setup: + Specify the following URL during the Runner setup: %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: %code #{@project.runners_token} %li - Start runner! + Start the Runner! - if @project_runners.any? diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 2d5b9f43c24..92957470070 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -2,24 +2,24 @@ .light.prepend-top-default %p - A 'runner' is a process which runs a build. - You can setup as many runners as you need. + A 'Runner' is a process which runs a build. + You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, and even on your local machine. - %p Each runner can be in one of the following states: + %p Each Runner can be in one of the following states: %div %ul %li %span.label.label-success active - \- runner is active and can process any new build + \- Runner is active and can process any new builds %li %span.label.label-danger paused - \- runner is paused and will not receive any new build + \- Runner is paused and will not receive any new builds %hr -%p.lead To start serving your builds you can either add specific runners to your project or use shared runners +%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners .row .col-sm-6 = render 'specific_runners' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9adce776c1c..ea4deb6cb28 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -71,9 +71,8 @@ = 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' + = render 'projects/buttons/download', project: @project, ref: @ref + = render 'projects/buttons/dropdown' = render 'shared/notifications/button', notification_setting: @notification_setting - if @repository.commit diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 4aa4ab46a2f..9773b8438ec 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,7 +1,7 @@ .hidden-xs - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do - New Snippet + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do + New snippet - if can?(current_user, :update_project_snippet, @snippet) = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete @@ -17,8 +17,8 @@ %ul - if can?(current_user, :create_project_snippet, @project) %li - = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do - New Snippet + = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New snippet" do + New snippet - if can?(current_user, :update_project_snippet, @snippet) %li = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 1646bcf4b8a..e77e1b026f6 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,10 +1,9 @@ - page_title "Snippets" .sub-header-block - .pull-right - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do - New Snippet + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do + New snippet .oneline Share code pastes with others out of git repository diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 37d341212af..9864be3562a 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -4,8 +4,8 @@ = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") -= render 'projects/last_push' = render "projects/commits/head" += render 'projects/last_push' %div{ class: container_class } .tree-controls diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 643f7c589e6..6624d5cb427 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -24,7 +24,7 @@ = succeed '.' do More examples are in the - = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown") + = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown") .form-group = f.label :commit_message, class: 'control-label' diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 551a20c1044..09c4411d67e 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,15 +1,16 @@ -.scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + = nav_link(path: 'wikis#pages') do + = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access + = nav_link(path: 'wikis#git_access') do + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + Git Access - = render 'projects/wikis/new' + = render 'projects/wikis/new' diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 249bce926ce..36bbac6fbf5 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -8,26 +8,26 @@ %b.caret %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li - = link_to page_filter_path(sort: sort_value_priority) do + = link_to page_filter_path(sort: sort_value_priority, label: true) do = sort_title_priority - = link_to page_filter_path(sort: sort_value_recently_created) do + = link_to page_filter_path(sort: sort_value_recently_created, label: true) do = sort_title_recently_created - = link_to page_filter_path(sort: sort_value_oldest_created) do + = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do = sort_title_oldest_created - = link_to page_filter_path(sort: sort_value_recently_updated) do + = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do = sort_title_recently_updated - = link_to page_filter_path(sort: sort_value_oldest_updated) do + = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do = sort_title_oldest_updated - = link_to page_filter_path(sort: sort_value_milestone_soon) do + = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do = sort_title_milestone_soon - = link_to page_filter_path(sort: sort_value_milestone_later) do + = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do = sort_title_milestone_later - if controller.controller_name == 'issues' || controller.action_name == 'issues' - = link_to page_filter_path(sort: sort_value_due_date_soon) do + = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do = sort_title_due_date_soon - = link_to page_filter_path(sort: sort_value_due_date_later) do + = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do = sort_title_due_date_later - = link_to page_filter_path(sort: sort_value_upvotes) do + = link_to page_filter_path(sort: sort_value_upvotes, label: true) do = sort_title_upvotes - = link_to page_filter_path(sort: sort_value_downvotes) do + = link_to page_filter_path(sort: sort_value_downvotes, label: true) do = sort_title_downvotes diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index add4536a0a2..b11257ee0e6 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -6,7 +6,7 @@ - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - else - .col-sm-10 + %div %span.info = visibility_level_icon(visibility_level) %strong diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index ebe2eb0433d..182c4eebd50 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -10,6 +10,6 @@ .option-descr = visibility_level_description(level, form_model) - unless restricted_visibility_levels.empty? - .col-sm-10 + %div %span.info Some visibility level settings have been restricted by the administrator. diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg new file mode 100644 index 00000000000..9228be05f03 --- /dev/null +++ b/app/views/shared/icons/_icon_empty_groups.svg @@ -0,0 +1 @@ +<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_no_wrap.svg b/app/views/shared/icons/_icon_no_wrap.svg new file mode 100644 index 00000000000..fe34cada002 --- /dev/null +++ b/app/views/shared/icons/_icon_no_wrap.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="m6 11h-4.509c-.263 0-.491.226-.491.505v.991c0 .291.22.505.491.505h4.509v.679c0 .301.194.413.454.236l2.355-1.607c.251-.171.259-.442 0-.619l-2.355-1.607c-.251-.171-.454-.07-.454.236v.681m-5-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m10 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991m-10-4c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991"/> +</svg> diff --git a/app/views/shared/icons/_icon_soft_wrap.svg b/app/views/shared/icons/_icon_soft_wrap.svg new file mode 100644 index 00000000000..ea27a2024b1 --- /dev/null +++ b/app/views/shared/icons/_icon_soft_wrap.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="m12 11h-2v-.681c0-.307-.203-.407-.454-.236l-2.355 1.607c-.259.177-.251.448 0 .619l2.355 1.607c.259.177.454.065.454-.236v-.679h2c0 0 0 0 0 0 1.657 0 3-1.343 3-3 0-.828-.336-1.578-.879-2.121-.543-.543-1.293-.879-2.121-.879-.001 0-.002 0-.002 0h-10.497c-.271 0-.5.226-.5.505v.991c0 .291.224.505.5.505h10.497c.001 0 .002 0 .002 0 .552 0 1 .448 1 1 0 .276-.112.526-.293.707-.181.181-.431.293-.707.293m-11-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m0 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991"/> +</svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f17096968f6..31620297be0 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,3 +1,4 @@ +- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder - boards_page = controller.controller_name == 'boards' .issues-filters @@ -14,19 +15,19 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter - = render "shared/issuable/milestone_dropdown" + = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown" + = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } .filter-item.inline.reset-filters %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 04373684ee9..c3f4e10c954 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,3 +1,4 @@ +- project = @target_project || @project = form_errors(issuable) - if @conflict @@ -82,38 +83,22 @@ = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) - %div - = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + - if issuable.assignee_id + = f.hidden_field :assignee_id + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee", show_menu_above: true } }) .form-group.issue-milestone = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } - - if milestone_options(issuable).present? - .issuable-form-select-holder - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - - else - .prepend-top-10 - %span.light No open milestones available. - - if can? current_user, :admin_milestone, issuable.project - %div - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .issuable-form-select-holder + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input" .form-group - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = f.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - - if has_labels - .issuable-form-select-holder - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - - else - %span.light No labels yet. - - if can? current_user, :admin_label, issuable.project - %div - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .issuable-form-select-holder + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' } - if has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index ebe441c931e..6d307611640 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,25 +1,29 @@ +- project = @target_project || @project - show_create = local_assigns.fetch(:show_create, true) - extra_options = local_assigns.fetch(:extra_options, true) - filter_submit = local_assigns.fetch(:filter_submit, true) - show_footer = local_assigns.fetch(:show_footer, true) +- use_id = local_assigns.fetch(:use_id, true) - data_options = local_assigns.fetch(:data_options, {}) - classes = local_assigns.fetch(:classes, []) -- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Label"} +- selected = local_assigns.fetch(:selected, nil) +- selected_toggle = local_assigns.fetch(:selected_toggle, nil) +- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) - classes << 'js-extra-options' if extra_options - classes << 'js-filter-submit' if filter_submit -- if params[:label_name].present? - - if params[:label_name].respond_to?('any?') - - params[:label_name].each do |label| - = hidden_field_tag "label_name[]", label, id: nil +- if selected + - selected.each do |label| + = 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} - %span.dropdown-toggle-text - = h(multi_label_name(params[:label_name], "Label")) + %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } + = multi_label_name(selected, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } - - if show_create and @project and can?(current_user, :admin_label, @project) + - if show_create && project && can?(current_user, :admin_label, project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 2fcf40ece99..ab3cc33d18f 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,16 +1,20 @@ -- if params[:milestone_title].present? - = hidden_field_tag(:milestone_title, params[:milestone_title]) -= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - - if @project +- project = @target_project || @project +- extra_class = extra_class || '' +- show_menu_above = show_menu_above || false +- selected_text = selected.try(:title) +- if selected.present? + = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) += dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", 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 + - if project %ul.dropdown-footer-list - - if can? current_user, :admin_milestone, @project + - if can? current_user, :admin_milestone, project %li - = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + = link_to new_namespace_project_milestone_path(project.namespace, project), title: "New Milestone" do Create new %li - = link_to namespace_project_milestones_path(@project.namespace, @project) do - - if can? current_user, :admin_milestone, @project + = link_to namespace_project_milestones_path(project.namespace, project) do + - if can? current_user, :admin_milestone, project Manage milestones - else View milestones diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 1d9b09a5ef1..5527a2f889a 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,25 +1,25 @@ +- type = local_assigns.fetch(:type, :issues) +- page_context_word = type.to_s.humanize(capitalize: false) +- issuables = @issues || @merge_requests + %ul.nav-links.issues-state-filters - - if defined?(type) && type == :merge_requests - - page_context_word = 'merge requests' - - else - - page_context_word = 'issues' %li{class: ("active" if params[:state] == 'opened')} = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do - #{state_filters_text_for(:opened, @project)} + #{issuables_state_counter_text(type, :opened)} - - if defined?(type) && type == :merge_requests + - if type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do - #{state_filters_text_for(:merged, @project)} + #{issuables_state_counter_text(type, :merged)} %li{class: ("active" if params[:state] == 'closed')} = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do - #{state_filters_text_for(:closed, @project)} + #{issuables_state_counter_text(type, :closed)} - else %li{class: ("active" if params[:state] == 'closed')} = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do - #{state_filters_text_for(:closed, @project)} + #{issuables_state_counter_text(type, :closed)} %li{class: ("active" if params[:state] == 'all')} = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do - #{state_filters_text_for(:all, @project)} + #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0464ebddc11..f8059988038 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -108,29 +108,30 @@ .js-due-date-calendar - if issuable.project.labels.any? + - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } = icon('tags') %span - = issuable.labels_array.size + = selected_labels.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } - - if issuable.labels_array.any? - - issuable.labels_array.each do |label| + .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } + - if selected_labels.any? + - selected_labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else %span.no-value None .selectbox.hide-collapsed - - issuable.labels_array.each do |label| + - 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{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", 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 - Label + %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 = render partial: "shared/issuable/label_page_default" diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index fdaca199218..c446dc3bdc1 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,7 +1,7 @@ .hidden-xs - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do - New Snippet + = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do + New snippet - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete @@ -16,8 +16,8 @@ .dropdown-menu.dropdown-menu-full-width %ul %li - = link_to new_snippet_path, title: "New Snippet" do - New Snippet + = link_to new_snippet_path, title: "New snippet" do + New snippet - if can?(current_user, :admin_personal_snippet, @snippet) %li = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 7be4a471579..77b66ca74b6 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,3 +1,5 @@ +- remote = local_assigns.fetch(:remote, false) + .snippets-list-holder %ul.content-list = render partial: 'shared/snippets/snippet', collection: @snippets @@ -5,7 +7,7 @@ %li .nothing-here-block Nothing here. - = paginate @snippets, theme: 'gitlab', remote: true + = paginate @snippets, theme: 'gitlab', remote: remote :javascript gl.SnippetsList(); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 2a57ac90bab..60fc0c0daf6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -36,7 +36,7 @@ .avatar-holder = link_to avatar_icon(@user, 400), target: '_blank' do = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' - + .user-info .cover-title = @user.name @@ -70,6 +70,10 @@ .profile-link-holder.middle-dot-divider = icon('map-marker') = @user.location + - unless @user.organization.blank? + .profile-link-holder.middle-dot-divider + = icon('building') + = @user.organization - if @user.bio.present? .cover-desc diff --git a/config/application.rb b/config/application.rb index f5c900da8cf..962ffe0708d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -101,13 +101,24 @@ module Gitlab config.action_view.sanitized_allowed_protocols = %w(smb) - config.middleware.use Rack::Attack + config.middleware.insert_before Warden::Manager, Rack::Attack # Allow access to GitLab API from other domains - config.middleware.use Rack::Cors do + config.middleware.insert_before Warden::Manager, Rack::Cors do + allow do + origins Gitlab.config.gitlab.url + resource '/api/*', + credentials: true, + headers: :any, + methods: :any, + expose: ['Link'] + end + + # Cross-origin requests must not have the session cookie available allow do origins '*' resource '/api/*', + credentials: false, headers: :any, methods: :any, expose: ['Link'] @@ -118,6 +129,10 @@ module Gitlab redis_config_hash = Gitlab::Redis.params redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever + if Sidekiq.server? # threaded context + redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5 + redis_config_hash[:pool_timeout] = 1 + end config.cache_store = :redis_store, redis_config_hash config.active_record.raise_in_transactional_callbacks = true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 1470a6e2550..a79356923b2 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -70,6 +70,7 @@ production: &base email_from: example@example.com email_display_name: GitLab email_reply_to: noreply@example.com + email_subject_suffix: '' # Email server smtp settings are in config/initializers/smtp_settings.rb.sample diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 195108b921b..c5ed2162c92 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -186,6 +186,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}" +Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || "" Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url) Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb new file mode 100644 index 00000000000..ae2ca258df1 --- /dev/null +++ b/config/initializers/7_redis.rb @@ -0,0 +1,3 @@ +# Make sure we initialize a Redis connection pool before Sidekiq starts +# multi-threaded execution. +Gitlab::Redis.with { nil } diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb index c668864089b..e007666b852 100644 --- a/config/initializers/attr_encrypted_no_db_connection.rb +++ b/config/initializers/attr_encrypted_no_db_connection.rb @@ -1,20 +1,21 @@ module AttrEncrypted module Adapters module ActiveRecord - def attribute_instance_methods_as_symbols_with_no_db_connection - # Use with_connection so the connection doesn't stay pinned to the thread. - connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false - - if connected - # Call version from AttrEncrypted::Adapters::ActiveRecord - attribute_instance_methods_as_symbols_without_no_db_connection - else - # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord - AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call + module DBConnectionQuerier + def attribute_instance_methods_as_symbols + # Use with_connection so the connection doesn't stay pinned to the thread. + connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false + + if connected + # Call version from AttrEncrypted::Adapters::ActiveRecord + super + else + # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord + AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call + end end end - - alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection + prepend DBConnectionQuerier end end end diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb index d831a1838ed..d0b1444f607 100644 --- a/config/initializers/connection_fix.rb +++ b/config/initializers/connection_fix.rb @@ -20,7 +20,7 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) execute_without_retry(*args) rescue ActiveRecord::StatementInvalid => e if e.message =~ /server has gone away/i - warn "Server timed out, retrying" + warn "Lost connection to MySQL server during query" reconnect! retry else diff --git a/config/initializers/postgresql_limit_fix.rb b/config/initializers/postgresql_limit_fix.rb index 0cb3aaf4d24..4224d857e8a 100644 --- a/config/initializers/postgresql_limit_fix.rb +++ b/config/initializers/postgresql_limit_fix.rb @@ -1,5 +1,19 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter + module LimitFilter + def add_column(table_name, column_name, type, options = {}) + options.delete(:limit) if type == :text + super(table_name, column_name, type, options) + end + + def change_column(table_name, column_name, type, options = {}) + options.delete(:limit) if type == :text + super(table_name, column_name, type, options) + end + end + + prepend ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::LimitFilter + class TableDefinition def text(*args) options = args.extract_options! @@ -9,18 +23,5 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) column_names.each { |name| column(name, type, options) } end end - - def add_column_with_limit_filter(table_name, column_name, type, options = {}) - options.delete(:limit) if type == :text - add_column_without_limit_filter(table_name, column_name, type, options) - end - - def change_column_with_limit_filter(table_name, column_name, type, options = {}) - options.delete(:limit) if type == :text - change_column_without_limit_filter(table_name, column_name, type, options) - end - - alias_method_chain :add_column, :limit_filter - alias_method_chain :change_column, :limit_filter end end diff --git a/config/routes.rb b/config/routes.rb index 4d6ec699cbd..bf7c5b76128 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,34 +2,13 @@ require 'sidekiq/web' require 'sidekiq/cron/web' require 'api/api' -Rails.application.routes.draw do - if Gitlab::Sherlock.enabled? - namespace :sherlock do - resources :transactions, only: [:index, :show] do - resources :queries, only: [:show] - resources :file_samples, only: [:show] - - collection do - delete :destroy_all - end - end - end - end - - if Rails.env.development? - # Make the built-in Rails routes available in development, otherwise they'd - # get swallowed by the `namespace/project` route matcher below. - # - # See https://git.io/va79N - get '/rails/mailers' => 'rails/mailers#index' - get '/rails/mailers/:path' => 'rails/mailers#preview' - get '/rails/info/properties' => 'rails/info#properties' - get '/rails/info/routes' => 'rails/info#routes' - get '/rails/info' => 'rails/info#index' - - mount LetterOpenerWeb::Engine, at: '/rails/letter_opener' +class ActionDispatch::Routing::Mapper + def draw(routes_name) + instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) end +end +Rails.application.routes.draw do concern :access_requestable do post :request_access, on: :collection post :approve_access_request, on: :member @@ -39,21 +18,9 @@ Rails.application.routes.draw do post :toggle_award_emoji, on: :member end - namespace :ci do - # CI API - Ci::API::API.logger Rails.logger - mount Ci::API::API => '/api' - - resource :lint, only: [:show, :create] - - resources :projects, only: [:index, :show] do - member do - get :status, to: 'projects#badge' - end - end - - root to: 'projects#index' - end + draw :sherlock + draw :development + draw :ci use_doorkeeper do controllers applications: 'oauth/applications', @@ -76,44 +43,18 @@ Rails.application.routes.draw do # JSON Web Token get 'jwt/auth' => 'jwt#auth' - # API - API::API.logger Rails.logger - mount API::API => '/api' - - constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? } - constraints constraint do - mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq - end - # Health check get 'health_check(/:checks)' => 'health_check#index', as: :health_check - # Help - get 'help' => 'help#index' - get 'help/shortcuts' => 'help#shortcuts' - get 'help/ui' => 'help#ui' - get 'help/*path' => 'help#show', as: :help_page - - # # Koding route - # get 'koding' => 'koding#index' - # - # Global snippets - # - resources :snippets, concerns: :awardable do - member do - get 'raw' - end - end - - get '/s/:username', to: redirect('/u/%{username}/snippets'), - constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } + draw :api + draw :sidekiq + draw :help + draw :snippets - # # Invites - # resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do member do post :accept @@ -127,809 +68,26 @@ Rails.application.routes.draw do end end - # # Spam reports - # resources :abuse_reports, only: [:new, :create] - # # Notification settings - # resources :notification_settings, only: [:create, :update] - # - # Import - # - namespace :import do - resource :github, only: [:create, :new], controller: :github do - post :personal_access_token - get :status - get :callback - get :jobs - end - - resource :gitlab, only: [:create], controller: :gitlab do - get :status - get :callback - get :jobs - end - - resource :bitbucket, only: [:create], controller: :bitbucket do - get :status - get :callback - get :jobs - end - - resource :google_code, only: [:create, :new], controller: :google_code do - get :status - post :callback - get :jobs - - get :new_user_map, path: :user_map - post :create_user_map, path: :user_map - end - - resource :fogbugz, only: [:create, :new], controller: :fogbugz do - get :status - post :callback - get :jobs - - get :new_user_map, path: :user_map - post :create_user_map, path: :user_map - end - - resource :gitlab_project, only: [:create, :new] do - post :create - end - end - - # - # Uploads - # - - scope path: :uploads do - # Note attachments and User/Group/Project avatars - get ":model/:mounted_as/:id/:filename", - to: "uploads#show", - constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } - - # Appearance - get ":model/:mounted_as/:id/:filename", - to: "uploads#show", - constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ } - - # Project markdown uploads - get ":namespace_id/:project_id/:secret/:filename", - to: "projects/uploads#show", - constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } - end - - # Redirect old note attachments path to new uploads path. - get "files/note/:id/:filename", - to: redirect("uploads/note/attachment/%{id}/%{filename}"), - constraints: { filename: /[^\/]+/ } - - # - # Explore area - # - namespace :explore do - resources :projects, only: [:index] do - collection do - get :trending - get :starred - end - end - - resources :groups, only: [:index] - resources :snippets, only: [:index] - root to: 'projects#trending' - end - - # Compatibility with old routing - get 'public' => 'explore/projects#index' - get 'public/projects' => 'explore/projects#index' - - # - # Admin Area - # - namespace :admin do - resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do - resources :keys, only: [:show, :destroy] - resources :identities, except: [:show] - - member do - get :projects - get :keys - get :groups - put :block - put :unblock - put :unlock - put :confirm - post :impersonate - patch :disable_two_factor - delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' - end - end - - resource :impersonation, only: :destroy - - resources :abuse_reports, only: [:index, :destroy] - resources :spam_logs, only: [:index, :destroy] do - member do - post :mark_as_ham - end - end - - resources :applications - - resources :groups, constraints: { id: /[^\/]+/ } do - member do - put :members_update - end - end - - resources :deploy_keys, only: [:index, :new, :create, :destroy] - - resources :hooks, only: [:index, :create, :destroy] do - get :test - end - - resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do - post :preview, on: :collection - end - - resource :logs, only: [:show] - resource :health_check, controller: 'health_check', only: [:show] - resource :background_jobs, controller: 'background_jobs', only: [:show] - 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, - path: '/', - constraints: { id: /[a-zA-Z.0-9_\-]+/ }, - only: [:index, :show]) do - root to: 'projects#show' - - member do - put :transfer - post :repository_check - end - - resources :runner_projects, only: [:create, :destroy] - end - end - - resource :appearances, only: [:show, :create, :update], path: 'appearance' do - member do - get :preview - delete :logo - delete :header_logos - end - end - - resource :application_settings, only: [:show, :update] do - resources :services, only: [:index, :edit, :update] - put :reset_runners_token - put :reset_health_check_token - put :clear_repository_check_states - end - - resources :labels - - resources :runners, only: [:index, :show, :update, :destroy] do - member do - get :resume - get :pause - end - end - - resources :builds, only: :index do - collection do - post :cancel_all - end - end - - root to: 'dashboard#index' - end - - # - # Profile Area - # - resource :profile, only: [:show, :update] do - member do - get :audit_log - get :applications, to: 'oauth/applications#index' - - put :reset_private_token - put :update_username - end - - scope module: :profiles do - resource :account, only: [:show] do - member do - delete :unlink - end - end - resource :notifications, only: [:show, :update] - resource :password, only: [:new, :create, :edit, :update] do - member do - put :reset - end - end - resource :preferences, only: [:show, :update] - resources :keys, only: [:index, :show, :new, :create, :destroy] - resources :emails, only: [:index, :create, :destroy] - resource :avatar, only: [:destroy] - - resources :personal_access_tokens, only: [:index, :create] do - member do - put :revoke - end - end - - resource :two_factor_auth, only: [:show, :create, :destroy] do - member do - post :create_u2f - post :codes - patch :skip - end - end - - resources :u2f_registrations, only: [:destroy] - end - end - - scope(path: 'u/:username', - as: :user, - constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, - controller: :users) do - get :calendar - get :calendar_activities - get :groups - get :projects - get :contributed, as: :contributed_projects - get :snippets - get '/', action: :show - end - - # - # Dashboard Area - # - resource :dashboard, controller: 'dashboard', only: [] do - get :issues - get :merge_requests - get :activity - - scope module: :dashboard do - resources :milestones, only: [:index, :show] - resources :labels, only: [:index] - - resources :groups, only: [:index] - resources :snippets, only: [:index] - - resources :todos, only: [:index, :destroy] do - collection do - delete :destroy_all - end - end - - resources :projects, only: [:index] do - collection do - get :starred - end - end - end - - root to: "dashboard/projects#index" - end - - # - # Groups Area - # - resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do - member do - get :issues - get :merge_requests - get :projects - get :activity - end - - scope module: :groups do - resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do - post :resend_invite, on: :member - delete :leave, on: :collection - end - - resource :avatar, only: [:destroy] - resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] - end - end - - resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create] - - devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, - registrations: :registrations, - passwords: :passwords, - sessions: :sessions, - confirmations: :confirmations } - - devise_scope :user do - get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error - get '/users/almost_there' => 'confirmations#almost_there' - end - - root to: "root#index" - - # - # Project Area - # - resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do - resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except: - [:new, :create, :index], path: "/") do - member do - put :transfer - delete :remove_fork - post :archive - post :unarchive - post :housekeeping - post :toggle_star - post :preview_markdown - post :export - post :remove_export - post :generate_new_export - get :download_export - get :autocomplete_sources - get :activity - get :refs - end - - scope module: :projects do - scope constraints: { id: /.+\.git/, format: nil } do - # Git HTTP clients ('git clone' etc.) - get '/info/refs', to: 'git_http#info_refs' - post '/git-upload-pack', to: 'git_http#git_upload_pack' - post '/git-receive-pack', to: 'git_http#git_receive_pack' - - # Git LFS API (metadata) - post '/info/lfs/objects/batch', to: 'lfs_api#batch' - post '/info/lfs/objects', to: 'lfs_api#deprecated' - get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' - - # GitLab LFS object storage - scope constraints: { oid: /[a-f0-9]{64}/ } do - get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' - - scope constraints: { size: /[0-9]+/ } do - put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' - put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' - end - end - end - - # Allow /info/refs, /info/refs?service=git-upload-pack, and - # /info/refs?service=git-receive-pack, but nothing else. - # - git_http_handshake = lambda do |request| - request.query_string.blank? || - request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/) - end - - ref_redirect = redirect do |params, request| - path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" - path << "?#{request.query_string}" unless request.query_string.blank? - path - end - - get '/info/refs', constraints: git_http_handshake, to: ref_redirect - - # Blob routes: - get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' - post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' - get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' - put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' - post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' - - # - # Templates - # - get '/templates/:template_type/:key' => 'templates#show', as: :template - - scope do - get( - '/blob/*id/diff', - to: 'blob#diff', - constraints: { id: /.+/, format: false }, - as: :blob_diff - ) - get( - '/blob/*id', - to: 'blob#show', - constraints: { id: /.+/, format: false }, - as: :blob - ) - delete( - '/blob/*id', - to: 'blob#destroy', - constraints: { id: /.+/, format: false } - ) - put( - '/blob/*id', - to: 'blob#update', - constraints: { id: /.+/, format: false } - ) - post( - '/blob/*id', - to: 'blob#create', - constraints: { id: /.+/, format: false } - ) - end - - scope do - get( - '/raw/*id', - to: 'raw#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :raw - ) - end - - scope do - get( - '/tree/*id', - to: 'tree#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :tree - ) - end - - scope do - get( - '/find_file/*id', - to: 'find_file#show', - constraints: { id: /.+/, format: /html/ }, - as: :find_file - ) - end - - scope do - get( - '/files/*id', - to: 'find_file#list', - constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, - as: :files - ) - end - - scope do - post( - '/create_dir/*id', - to: 'tree#create_dir', - constraints: { id: /.+/ }, - as: 'create_dir' - ) - end - - scope do - get( - '/blame/*id', - to: 'blame#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :blame - ) - end - - scope do - get( - '/commits/*id', - to: 'commits#show', - constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }, - as: :commits - ) - end - - resource :avatar, only: [:show, :destroy] - resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do - member do - get :branches - get :builds - post :cancel_builds - post :retry_builds - post :revert - post :cherry_pick - get :diff_for_path - end - end - - resources :compare, only: [:index, :create] do - collection do - get :diff_for_path - end - end - - get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } - - # Don't use format parameter as file extension (old 3.0.x behavior) - # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments - scope format: false do - resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } - - resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do - member do - get :commits - get :ci - get :languages - end - end - end - - resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do - member do - get 'raw' - end - end - - WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID - - scope do - # Order matters to give priority to these matches - get '/wikis/git_access', to: 'wikis#git_access' - get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' - post '/wikis', to: 'wikis#create' - - get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID - get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID - - get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID - delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID - put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID - post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown' - end - - resource :repository, only: [:create] do - member do - get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } - end - end - - resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do - member do - get :test - end - end - - resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do - member do - put :enable - put :disable - end - end - - resources :forks, only: [:index, :new, :create] - resource :import, only: [:new, :create, :show] - - resources :refs, only: [] do - collection do - get 'switch' - end - - member do - # tree viewer logs - get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } - # Directories with leading dots erroneously get rejected if git - # ref regex used in constraints. Regex verification now done in controller. - get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { - id: /.*/, - path: /.*/ - } - end - end - - resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do - member do - get :commits - get :diffs - get :conflicts - get :builds - get :pipelines - get :merge_check - post :merge - post :cancel_merge_when_build_succeeds - get :ci_status - post :toggle_subscription - post :remove_wip - get :diff_for_path - post :resolve_conflicts - end - - collection do - get :branch_from - get :branch_to - get :update_branches - get :diff_for_path - post :bulk_update - end - - resources :discussions, only: [], constraints: { id: /\h{40}/ } do - member do - post :resolve - delete :resolve, action: :unresolve - end - end - end - - resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do - resource :release, only: [:edit, :update] - end - - resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :variables, only: [:index, :show, :update, :create, :destroy] - resources :triggers, only: [:index, :create, :destroy] - - resources :pipelines, only: [:index, :new, :create, :show] do - collection do - resource :pipelines_settings, path: 'settings', only: [:show, :update] - end - - member do - post :cancel - post :retry - end - end - - resources :environments - - resource :cycle_analytics, only: [:show] - - resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do - collection do - post :cancel_all - - resources :artifacts, only: [] do - collection do - get :latest_succeeded, - path: '*ref_name_and_path', - format: false - end - end - end - - member do - get :status - post :cancel - post :retry - post :play - post :erase - get :trace - get :raw - end - - resource :artifacts, only: [] do - get :download - get :browse, path: 'browse(/*path)', format: false - get :file, path: 'file/*path', format: false - post :keep - end - end - - resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do - member do - get :test - end - end - - resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex } - - resources :milestones, constraints: { id: /\d+/ } do - member do - put :sort_issues - put :sort_merge_requests - end - end - - resources :labels, except: [:show], constraints: { id: /\d+/ } do - collection do - post :generate - post :set_priorities - end - - member do - post :toggle_subscription - delete :remove_priority - end - end - - resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do - member do - post :toggle_subscription - post :mark_as_spam - get :referenced_merge_requests - get :related_branches - get :can_create_branch - end - collection do - post :bulk_update - end - end - - resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do - collection do - delete :leave - - # Used for import team - # from another project - get :import - post :apply_import - end - - member do - post :resend_invite - end - end - - resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } - - resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do - member do - delete :delete_attachment - post :resolve - delete :resolve, action: :unresolve - end - end - - resource :board, only: [:show] do - scope module: :boards do - resources :issues, only: [:update] - - resources :lists, only: [:index, :create, :update, :destroy] do - collection do - post :generate - end - - resources :issues, only: [:index] - end - end - end - - resources :todos, only: [:create] - - resources :uploads, only: [:create] do - collection do - get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } - end - end - - resources :runners, only: [:index, :edit, :update, :destroy, :show] do - member do - get :resume - get :pause - end - - collection do - post :toggle_shared_runners - end - end - - resources :runner_projects, only: [:create, :destroy] - resources :badges, only: [:index] do - collection do - scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do - constraints format: /svg/ do - get :build - get :coverage - end - end - end - end - end - end - end + draw :import + draw :uploads + draw :explore + draw :admin + draw :profile + draw :dashboard + draw :group + draw :user + draw :project # Get all keys of user get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ } get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } + + root to: "root#index" end diff --git a/config/routes/admin.rb b/config/routes/admin.rb new file mode 100644 index 00000000000..5ae985da561 --- /dev/null +++ b/config/routes/admin.rb @@ -0,0 +1,102 @@ +namespace :admin do + resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do + resources :keys, only: [:show, :destroy] + resources :identities, except: [:show] + + member do + get :projects + get :keys + get :groups + put :block + put :unblock + put :unlock + put :confirm + post :impersonate + patch :disable_two_factor + delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' + end + end + + resource :impersonation, only: :destroy + + resources :abuse_reports, only: [:index, :destroy] + resources :spam_logs, only: [:index, :destroy] do + member do + post :mark_as_ham + end + end + + resources :applications + + resources :groups, constraints: { id: /[^\/]+/ } do + member do + put :members_update + end + end + + resources :deploy_keys, only: [:index, :new, :create, :destroy] + + resources :hooks, only: [:index, :create, :destroy] do + get :test + end + + resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do + post :preview, on: :collection + end + + resource :logs, only: [:show] + resource :health_check, controller: 'health_check', only: [:show] + resource :background_jobs, controller: 'background_jobs', only: [:show] + 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, + path: '/', + constraints: { id: /[a-zA-Z.0-9_\-]+/ }, + only: [:index, :show]) do + root to: 'projects#show' + + member do + put :transfer + post :repository_check + end + + resources :runner_projects, only: [:create, :destroy] + end + end + + resource :appearances, only: [:show, :create, :update], path: 'appearance' do + member do + get :preview + delete :logo + delete :header_logos + end + end + + resource :application_settings, only: [:show, :update] do + resources :services, only: [:index, :edit, :update] + put :reset_runners_token + put :reset_health_check_token + put :clear_repository_check_states + end + + resources :labels + + resources :runners, only: [:index, :show, :update, :destroy] do + member do + get :resume + get :pause + end + end + + resources :builds, only: :index do + collection do + post :cancel_all + end + end + + root to: 'dashboard#index' +end diff --git a/config/routes/api.rb b/config/routes/api.rb new file mode 100644 index 00000000000..69c8efc151c --- /dev/null +++ b/config/routes/api.rb @@ -0,0 +1,2 @@ +API::API.logger Rails.logger +mount API::API => '/api' diff --git a/config/routes/ci.rb b/config/routes/ci.rb new file mode 100644 index 00000000000..47a049d5b20 --- /dev/null +++ b/config/routes/ci.rb @@ -0,0 +1,15 @@ +namespace :ci do + # CI API + Ci::API::API.logger Rails.logger + mount Ci::API::API => '/api' + + resource :lint, only: [:show, :create] + + resources :projects, only: [:index, :show] do + member do + get :status, to: 'projects#badge' + end + end + + root to: 'projects#index' +end diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb new file mode 100644 index 00000000000..fb20c63bc63 --- /dev/null +++ b/config/routes/dashboard.rb @@ -0,0 +1,27 @@ +resource :dashboard, controller: 'dashboard', only: [] do + get :issues + get :merge_requests + get :activity + + scope module: :dashboard do + resources :milestones, only: [:index, :show] + resources :labels, only: [:index] + + resources :groups, only: [:index] + resources :snippets, only: [:index] + + resources :todos, only: [:index, :destroy] do + collection do + delete :destroy_all + end + end + + resources :projects, only: [:index] do + collection do + get :starred + end + end + end + + root to: "dashboard/projects#index" +end diff --git a/config/routes/development.rb b/config/routes/development.rb new file mode 100644 index 00000000000..9b2b47c6a21 --- /dev/null +++ b/config/routes/development.rb @@ -0,0 +1,13 @@ +if Rails.env.development? + # Make the built-in Rails routes available in development, otherwise they'd + # get swallowed by the `namespace/project` route matcher below. + # + # See https://git.io/va79N + get '/rails/mailers' => 'rails/mailers#index' + get '/rails/mailers/:path' => 'rails/mailers#preview' + get '/rails/info/properties' => 'rails/info#properties' + get '/rails/info/routes' => 'rails/info#routes' + get '/rails/info' => 'rails/info#index' + + mount LetterOpenerWeb::Engine, at: '/rails/letter_opener' +end diff --git a/config/routes/explore.rb b/config/routes/explore.rb new file mode 100644 index 00000000000..42ec5e8abec --- /dev/null +++ b/config/routes/explore.rb @@ -0,0 +1,16 @@ +namespace :explore do + resources :projects, only: [:index] do + collection do + get :trending + get :starred + end + end + + resources :groups, only: [:index] + resources :snippets, only: [:index] + root to: 'projects#trending' +end + +# Compatibility with old routing +get 'public' => 'explore/projects#index' +get 'public/projects' => 'explore/projects#index' diff --git a/config/routes/group.rb b/config/routes/group.rb new file mode 100644 index 00000000000..5b3e25d5e3d --- /dev/null +++ b/config/routes/group.rb @@ -0,0 +1,18 @@ +resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do + member do + get :issues + get :merge_requests + get :projects + get :activity + end + + scope module: :groups do + resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do + post :resend_invite, on: :member + delete :leave, on: :collection + end + + resource :avatar, only: [:destroy] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + end +end diff --git a/config/routes/help.rb b/config/routes/help.rb new file mode 100644 index 00000000000..d53822da9ec --- /dev/null +++ b/config/routes/help.rb @@ -0,0 +1,4 @@ +get 'help' => 'help#index' +get 'help/shortcuts' => 'help#shortcuts' +get 'help/ui' => 'help#ui' +get 'help/*path' => 'help#show', as: :help_page diff --git a/config/routes/import.rb b/config/routes/import.rb new file mode 100644 index 00000000000..89f3b3f6378 --- /dev/null +++ b/config/routes/import.rb @@ -0,0 +1,42 @@ +namespace :import do + resource :github, only: [:create, :new], controller: :github do + post :personal_access_token + get :status + get :callback + get :jobs + end + + resource :gitlab, only: [:create], controller: :gitlab do + get :status + get :callback + get :jobs + end + + resource :bitbucket, only: [:create], controller: :bitbucket do + get :status + get :callback + get :jobs + end + + resource :google_code, only: [:create, :new], controller: :google_code do + get :status + post :callback + get :jobs + + get :new_user_map, path: :user_map + post :create_user_map, path: :user_map + end + + resource :fogbugz, only: [:create, :new], controller: :fogbugz do + get :status + post :callback + get :jobs + + get :new_user_map, path: :user_map + post :create_user_map, path: :user_map + end + + resource :gitlab_project, only: [:create, :new] do + post :create + end +end diff --git a/config/routes/profile.rb b/config/routes/profile.rb new file mode 100644 index 00000000000..4cb68c9b34a --- /dev/null +++ b/config/routes/profile.rb @@ -0,0 +1,43 @@ +resource :profile, only: [:show, :update] do + member do + get :audit_log + get :applications, to: 'oauth/applications#index' + + put :reset_private_token + put :update_username + end + + scope module: :profiles do + resource :account, only: [:show] do + member do + delete :unlink + end + end + resource :notifications, only: [:show, :update] + resource :password, only: [:new, :create, :edit, :update] do + member do + put :reset + end + end + resource :preferences, only: [:show, :update] + resources :keys, only: [:index, :show, :new, :create, :destroy] + resources :emails, only: [:index, :create, :destroy] + resource :avatar, only: [:destroy] + + resources :personal_access_tokens, only: [:index, :create] do + member do + put :revoke + end + end + + resource :two_factor_auth, only: [:show, :create, :destroy] do + member do + post :create_u2f + post :codes + patch :skip + end + end + + resources :u2f_registrations, only: [:destroy] + end +end diff --git a/config/routes/project.rb b/config/routes/project.rb new file mode 100644 index 00000000000..224ec7e8324 --- /dev/null +++ b/config/routes/project.rb @@ -0,0 +1,464 @@ +resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create] + +resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do + resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except: + [:new, :create, :index], path: "/") do + member do + put :transfer + delete :remove_fork + post :archive + post :unarchive + post :housekeeping + post :toggle_star + post :preview_markdown + post :export + post :remove_export + post :generate_new_export + get :download_export + get :autocomplete_sources + get :activity + get :refs + end + + scope module: :projects do + scope constraints: { id: /.+\.git/, format: nil } do + # Git HTTP clients ('git clone' etc.) + get '/info/refs', to: 'git_http#info_refs' + post '/git-upload-pack', to: 'git_http#git_upload_pack' + post '/git-receive-pack', to: 'git_http#git_receive_pack' + + # Git LFS API (metadata) + post '/info/lfs/objects/batch', to: 'lfs_api#batch' + post '/info/lfs/objects', to: 'lfs_api#deprecated' + get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' + + # GitLab LFS object storage + scope constraints: { oid: /[a-f0-9]{64}/ } do + get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' + + scope constraints: { size: /[0-9]+/ } do + put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' + put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' + end + end + end + + # Allow /info/refs, /info/refs?service=git-upload-pack, and + # /info/refs?service=git-receive-pack, but nothing else. + # + git_http_handshake = lambda do |request| + request.query_string.blank? || + request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/) + end + + ref_redirect = redirect do |params, request| + path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" + path << "?#{request.query_string}" unless request.query_string.blank? + path + end + + get '/info/refs', constraints: git_http_handshake, to: ref_redirect + + # Blob routes: + get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' + post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' + get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' + put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' + post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' + + # + # Templates + # + get '/templates/:template_type/:key' => 'templates#show', as: :template + + scope do + get( + '/blob/*id/diff', + to: 'blob#diff', + constraints: { id: /.+/, format: false }, + as: :blob_diff + ) + get( + '/blob/*id', + to: 'blob#show', + constraints: { id: /.+/, format: false }, + as: :blob + ) + delete( + '/blob/*id', + to: 'blob#destroy', + constraints: { id: /.+/, format: false } + ) + put( + '/blob/*id', + to: 'blob#update', + constraints: { id: /.+/, format: false } + ) + post( + '/blob/*id', + to: 'blob#create', + constraints: { id: /.+/, format: false } + ) + end + + scope do + get( + '/raw/*id', + to: 'raw#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :raw + ) + end + + scope do + get( + '/tree/*id', + to: 'tree#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :tree + ) + end + + scope do + get( + '/find_file/*id', + to: 'find_file#show', + constraints: { id: /.+/, format: /html/ }, + as: :find_file + ) + end + + scope do + get( + '/files/*id', + to: 'find_file#list', + constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, + as: :files + ) + end + + scope do + post( + '/create_dir/*id', + to: 'tree#create_dir', + constraints: { id: /.+/ }, + as: 'create_dir' + ) + end + + scope do + get( + '/blame/*id', + to: 'blame#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :blame + ) + end + + scope do + get( + '/commits/*id', + to: 'commits#show', + constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }, + as: :commits + ) + end + + resource :avatar, only: [:show, :destroy] + 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 + end + end + + resources :compare, only: [:index, :create] do + collection do + get :diff_for_path + end + end + + get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } + + # Don't use format parameter as file extension (old 3.0.x behavior) + # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments + scope format: false do + resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } + + resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do + member do + get :commits + get :ci + get :languages + end + end + end + + resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do + member do + get 'raw' + end + end + + WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID + + scope do + # Order matters to give priority to these matches + get '/wikis/git_access', to: 'wikis#git_access' + get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' + post '/wikis', to: 'wikis#create' + + get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID + get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID + + get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID + delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID + put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID + post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown' + end + + resource :repository, only: [:create] do + member do + get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } + end + end + + resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do + member do + get :test + end + end + + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do + member do + put :enable + put :disable + end + end + + resources :forks, only: [:index, :new, :create] + resource :import, only: [:new, :create, :show] + + resources :refs, only: [] do + collection do + get 'switch' + end + + member do + # tree viewer logs + get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + # Directories with leading dots erroneously get rejected if git + # ref regex used in constraints. Regex verification now done in controller. + get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { + id: /.*/, + path: /.*/ + } + end + end + + resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do + member do + get :commits + get :diffs + get :conflicts + get :builds + get :pipelines + get :merge_check + post :merge + post :cancel_merge_when_build_succeeds + get :ci_status + post :toggle_subscription + post :remove_wip + get :diff_for_path + post :resolve_conflicts + end + + collection do + get :branch_from + get :branch_to + get :update_branches + get :diff_for_path + post :bulk_update + end + + resources :discussions, only: [], constraints: { id: /\h{40}/ } do + member do + post :resolve + delete :resolve, action: :unresolve + end + end + end + + resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do + resource :release, only: [:edit, :update] + end + + resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :variables, only: [:index, :show, :update, :create, :destroy] + resources :triggers, only: [:index, :create, :destroy] + + resources :pipelines, only: [:index, :new, :create, :show] do + collection do + resource :pipelines_settings, path: 'settings', only: [:show, :update] + end + + member do + post :cancel + post :retry + end + end + + resources :environments + + resource :cycle_analytics, only: [:show] + + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do + collection do + post :cancel_all + + resources :artifacts, only: [] do + collection do + get :latest_succeeded, + path: '*ref_name_and_path', + format: false + end + end + end + + member do + get :status + post :cancel + post :retry + post :play + post :erase + get :trace + get :raw + end + + resource :artifacts, only: [] do + get :download + get :browse, path: 'browse(/*path)', format: false + get :file, path: 'file/*path', format: false + post :keep + end + end + + resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do + member do + get :test + end + end + + resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex } + + resources :milestones, constraints: { id: /\d+/ } do + member do + put :sort_issues + put :sort_merge_requests + end + end + + resources :labels, except: [:show], constraints: { id: /\d+/ } do + collection do + post :generate + post :set_priorities + end + + member do + post :toggle_subscription + delete :remove_priority + end + end + + resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do + member do + post :toggle_subscription + post :mark_as_spam + get :referenced_merge_requests + get :related_branches + get :can_create_branch + end + collection do + post :bulk_update + end + end + + resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do + collection do + delete :leave + + # Used for import team + # from another project + get :import + post :apply_import + end + + member do + post :resend_invite + end + end + + resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } + + resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do + member do + delete :delete_attachment + post :resolve + delete :resolve, action: :unresolve + end + end + + resource :board, only: [:show] do + scope module: :boards do + resources :issues, only: [:update] + + resources :lists, only: [:index, :create, :update, :destroy] do + collection do + post :generate + end + + resources :issues, only: [:index] + end + end + end + + resources :todos, only: [:create] + + resources :uploads, only: [:create] do + collection do + get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } + end + end + + resources :runners, only: [:index, :edit, :update, :destroy, :show] do + member do + get :resume + get :pause + end + + collection do + post :toggle_shared_runners + end + end + + resources :runner_projects, only: [:create, :destroy] + resources :badges, only: [:index] do + collection do + scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do + constraints format: /svg/ do + get :build + get :coverage + end + end + end + end + end + end +end diff --git a/config/routes/sherlock.rb b/config/routes/sherlock.rb new file mode 100644 index 00000000000..c9969f91c36 --- /dev/null +++ b/config/routes/sherlock.rb @@ -0,0 +1,12 @@ +if Gitlab::Sherlock.enabled? + namespace :sherlock do + resources :transactions, only: [:index, :show] do + resources :queries, only: [:show] + resources :file_samples, only: [:show] + + collection do + delete :destroy_all + end + end + end +end diff --git a/config/routes/sidekiq.rb b/config/routes/sidekiq.rb new file mode 100644 index 00000000000..d3e6bc4c292 --- /dev/null +++ b/config/routes/sidekiq.rb @@ -0,0 +1,4 @@ +constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? } +constraints constraint do + mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq +end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb new file mode 100644 index 00000000000..1949f215c66 --- /dev/null +++ b/config/routes/snippets.rb @@ -0,0 +1,8 @@ +resources :snippets, concerns: :awardable do + member do + get 'raw' + end +end + +get '/s/:username', to: redirect('/u/%{username}/snippets'), + constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb new file mode 100644 index 00000000000..2b22148a134 --- /dev/null +++ b/config/routes/uploads.rb @@ -0,0 +1,21 @@ +scope path: :uploads do + # Note attachments and User/Group/Project avatars + get ":model/:mounted_as/:id/:filename", + to: "uploads#show", + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } + + # Appearance + get ":model/:mounted_as/:id/:filename", + to: "uploads#show", + constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ } + + # Project markdown uploads + get ":namespace_id/:project_id/:secret/:filename", + to: "projects/uploads#show", + constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } +end + +# Redirect old note attachments path to new uploads path. +get "files/note/:id/:filename", + to: redirect("uploads/note/attachment/%{id}/%{filename}"), + constraints: { filename: /[^\/]+/ } diff --git a/config/routes/user.rb b/config/routes/user.rb new file mode 100644 index 00000000000..bbb30cedd4d --- /dev/null +++ b/config/routes/user.rb @@ -0,0 +1,23 @@ +scope(path: 'u/:username', + as: :user, + constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, + controller: :users) do + get :calendar + get :calendar_activities + get :groups + get :projects + get :contributed, as: :contributed_projects + get :snippets + get '/', action: :show +end + +devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, + registrations: :registrations, + passwords: :passwords, + sessions: :sessions, + confirmations: :confirmations } + +devise_scope :user do + get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error + get '/users/almost_there' => 'confirmations#almost_there' +end diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb index 3e8cdcd67b4..9739a5ac8d5 100644 --- a/db/fixtures/development/06_teams.rb +++ b/db/fixtures/development/06_teams.rb @@ -1,7 +1,7 @@ Gitlab::Seeder.quiet do Group.all.each do |group| User.all.sample(4).each do |user| - if group.add_users([user.id], Gitlab::Access.values.sample) + if group.add_user(user, Gitlab::Access.values.sample).persisted? print '.' else print 'F' diff --git a/db/migrate/20160926145521_add_organization_to_user.rb b/db/migrate/20160926145521_add_organization_to_user.rb new file mode 100644 index 00000000000..e0bef6e7548 --- /dev/null +++ b/db/migrate/20160926145521_add_organization_to_user.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 AddOrganizationToUser < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :users, :organization, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 425fc33b7b3..ad62c249b3f 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: 20160920160832) do +ActiveRecord::Schema.define(version: 20160926145521) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1132,6 +1132,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false + t.string "organization" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/README.md b/doc/README.md index dd0eb97489e..4ff1a0582c8 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,6 +19,7 @@ - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. +- [University](university/README.md) Learn Git and GitLab through videos and courses. ## Administrator documentation diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 7186f707ad6..bf7814875bf 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -275,3 +275,9 @@ If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. + +### Login with valid credentials rejected + +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 +`production.log`. diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 7f53915a4d7..b4a953d1ccc 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,15 +13,17 @@ 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_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 ## Complete database variables diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index 34b4f1faa94..ad1fa98b63b 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -12,7 +12,7 @@ revisions (to reduce disk space and increase performance) and removing unreachable objects which may have been created from prior invocations of `git add`. -You can find this option under your **[Project] > Settings**. +You can find this option under your **[Project] > Edit Project**. --- diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png Binary files differindex f72ad9a45d5..6ebc6205635 100644 --- a/doc/administration/img/housekeeping_settings.png +++ b/doc/administration/img/housekeeping_settings.png diff --git a/doc/api/README.md b/doc/api/README.md index 8e4f7f12b4b..bbd5bcfb386 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -101,7 +101,7 @@ Once you have your token, pass it to the API using either the `private_token` parameter or the `PRIVATE-TOKEN` header. -###Â Session cookie +### Session Cookie When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is set. The API will use this cookie for authentication if it is present, but using diff --git a/doc/api/builds.md b/doc/api/builds.md index dce666445d0..e8a9e4743d3 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -40,6 +40,12 @@ Example of response "finished_at": "2015-12-24T17:54:27.895Z", "id": 7, "name": "teaspoon", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + } "ref": "master", "runner": null, "stage": "test", @@ -78,6 +84,12 @@ Example of response "finished_at": "2015-12-24T17:54:24.921Z", "id": 6, "name": "spinach:other", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + } "ref": "master", "runner": null, "stage": "test", @@ -146,6 +158,12 @@ Example of response "finished_at": "2016-01-11T10:14:09.526Z", "id": 69, "name": "rubocop", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + } "ref": "master", "runner": null, "stage": "test", @@ -170,6 +188,12 @@ Example of response "finished_at": "2015-12-24T17:54:33.913Z", "id": 9, "name": "brakeman", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + } "ref": "master", "runner": null, "stage": "test", @@ -231,6 +255,12 @@ Example of response "finished_at": "2015-12-24T17:54:31.198Z", "id": 8, "name": "rubocop", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + } "ref": "master", "runner": null, "stage": "test", diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 0b0fc39ec7e..5ef5e3f5744 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -1,10 +1,10 @@ -# GitLab as an OAuth2 client +# GitLab as an OAuth2 provider This document covers using the OAuth2 protocol to access GitLab. If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md). -OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party. +OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party. This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper) @@ -22,7 +22,7 @@ In the following sections you will be introduced to the three steps needed for t ### 1. Registering the client First, you should create an application (`/profile/applications`) in your user's account. -Each application gets a unique App ID and App Secret parameters. +Each application gets a unique App ID and App Secret parameters. >**Note:** **You should not share/leak your App ID or App Secret.** @@ -46,10 +46,10 @@ http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash You should then use the `code` to request an access token. >**Important:** -It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and -validate that value is returned and matches in the redirect request. -This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow), -`state` really should have been a requirement in the standard! +It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and +validate that value is returned and matches in the redirect request. +This is important to prevent [CSRF attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow), +`state` really should have been a requirement in the standard! ### 3. Requesting the access token @@ -62,7 +62,7 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters # The response will be { "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", - "token_type": "bearer", + "token_type": "bearer", "expires_in": 7200, "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" } @@ -95,7 +95,7 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/ --- -In this flow, a token is requested in exchange for the resource owner credentials (username and password). +In this flow, a token is requested in exchange for the resource owner credentials (username and password). The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the client is part of the device operating system or a highly privileged application), and when other authorization grant types are not available (such as an authorization code). @@ -112,7 +112,7 @@ You can do POST request to `/oauth/token` with parameters: { "grant_type" : "password", "username" : "user@example.com", - "password" : "sekret" + "password" : "secret" } ``` @@ -130,8 +130,8 @@ For testing you can use the oauth2 ruby gem: ``` client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com") -access_token = client.password.get_token('user@example.com', 'sekret') +access_token = client.password.get_token('user@example.com', 'secret') puts access_token.token ``` -[personal access tokens]: ./README.md#personal-access-tokens +[personal access tokens]: ./README.md#personal-access-tokens
\ No newline at end of file diff --git a/doc/api/projects.md b/doc/api/projects.md index 750ce1508df..869907b0dd7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -899,6 +899,7 @@ Parameters: - `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `group_id` (required) - The ID of a group - `group_access` (required) - Level of permissions for sharing +- `expires_at` - Share expiration date in ISO 8601 format: 2016-09-26 ## Hooks diff --git a/doc/api/settings.md b/doc/api/settings.md index aaa2c99642b..f7ad3b4cc8e 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -41,7 +41,9 @@ Example response: "gravatar_enabled" : true, "sign_in_text" : null, "container_registry_token_expire_delay": 5, - "repository_storage": "default" + "repository_storage": "default", + "koding_enabled": false, + "koding_url": null } ``` @@ -72,7 +74,9 @@ PUT /application/settings | `after_sign_out_path` | string | no | Where to redirect users after logout | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml | -| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. +| `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. | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -103,6 +107,8 @@ Example response: "user_oauth_applications": true, "after_sign_out_path": "", "container_registry_token_expire_delay": 5, - "repository_storage": "default" + "repository_storage": "default", + "koding_enabled": false, + "koding_url": null } ``` diff --git a/doc/api/users.md b/doc/api/users.md index 54f7a2a2ace..9be4f2e6ec3 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -57,6 +57,7 @@ GET /users "linkedin": "", "twitter": "", "website_url": "", + "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, @@ -89,6 +90,7 @@ GET /users "linkedin": "", "twitter": "", "website_url": "", + "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", "theme_id": 1, @@ -147,7 +149,8 @@ Parameters: "skype": "", "linkedin": "", "twitter": "", - "website_url": "" + "website_url": "", + "organization": "" } ``` @@ -178,6 +181,7 @@ Parameters: "linkedin": "", "twitter": "", "website_url": "", + "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, @@ -214,6 +218,7 @@ Parameters: - `linkedin` (optional) - LinkedIn - `twitter` (optional) - Twitter account - `website_url` (optional) - Website URL +- `organization` (optional) - Organization name - `projects_limit` (optional) - Number of projects user can create - `extern_uid` (optional) - External UID - `provider` (optional) - External provider name @@ -242,6 +247,7 @@ Parameters: - `linkedin` - LinkedIn - `twitter` - Twitter account - `website_url` - Website URL +- `organization` - Organization name - `projects_limit` - Limit projects each user can create - `extern_uid` - External UID - `provider` - External provider name @@ -296,6 +302,7 @@ GET /user "linkedin": "", "twitter": "", "website_url": "", + "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index a849905ac6b..520c8b36a95 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -221,7 +221,7 @@ time. *Note: The following commands are run without root privileges. You should be able to run docker with your regular user account.* -First start with creating a file named `build script`: +First start with creating a file named `build_script`: ```bash cat <<EOF > build_script diff --git a/doc/ci/environments.md b/doc/ci/environments.md index d85b8a34ced..e070302fb82 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track Deployments are created when [jobs] deploy versions of code to [environments]. +### 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. + +In your git config, append the `[remote "<your-remote>"]` block with an extra +fetch line: + +``` +fetch = +refs/environments/*:refs/remotes/origin/environments/* +``` + ## Defining environments You can create and delete environments manually in the web interface, but we diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index b78422f6d0e..84048f1d25f 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -4,7 +4,7 @@ > **Note**: GitLab 8.12 has a completely redesigned build permissions system. -Read all about the [new model and its implications][../../user/project/new_ci_build_permissions_model.md#build-triggers]. +Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers). Triggers can be used to force a rebuild of a specific branch, tag or commit, with an API call. diff --git a/doc/development/README.md b/doc/development/README.md index 58c00f618fa..9706cb1de7f 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -13,6 +13,7 @@ - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [Testing standards and style guidelines](testing.md) - [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements +- [Frontend guidelines](frontend.md) - [SQL guidelines](sql.md) for SQL guidelines ## Process diff --git a/doc/development/frontend.md b/doc/development/frontend.md new file mode 100644 index 00000000000..f879cd57e25 --- /dev/null +++ b/doc/development/frontend.md @@ -0,0 +1,225 @@ +# Frontend Development Guidelines + +This document describes various guidelines to ensure consistency and quality +across GitLab's frontend team. + +## Overview + +GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with +[Hamlit][hamlit]. Be wary of [the limitations that come with using +Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with +[ES6 by way of Babel][es6]. + +The asset pipeline is [Sprockets][sprockets], which handles the concatenation, +minification, and compression of our assets. + +[jQuery][jquery] is used throughout the application's JavaScript, with +[Vue.js][vue] for particularly advanced, dynamic elements. + +### Vue + +For more complex frontend features, we recommend using Vue.js. It shares +some ideas with React.js as well as Angular. + +To get started with Vue, read through [their documentation][vue-docs]. + +## Performance + +### Resources + +- [WebPage Test][web-page-test] for testing site loading time and size. +- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page. +- [Profiling with Chrome DevTools][google-devtools-profiling] +- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance. + +### Page-specific JavaScript + +Certain pages may require the use of a third party library, such as [d3][d3] for +the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These +libraries increase the page size significantly, and impact load times due to +bandwidth bottlenecks and the browser needing to parse more JavaScript. + +In cases where libraries are only used on a few specific pages, we use +"page-specific JavaScript" to prevent the main `application.js` file from +becoming unnecessarily large. + +Steps to split page-specific JavaScript from the main `application.js`: + +1. Create a directory for the specific page(s), e.g. `graphs/`. +1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`. +1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle. +1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added. +1. Add the new "bundle" file to the list of precompiled assets in +`config/application.rb`. + - For example: `config.assets.precompile << "graphs/graphs_bundle.js"`. +1. Move code reliant on these libraries into the `graphs` directory. +1. In the relevant views, add the scripts to the page with the following: + +```haml +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/chart.js') + = page_specific_javascript_tag('graphs/graphs_bundle.js') +``` + +The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js` +is separated from the bundle file so it can be cached separately from the bundle +and reused for other pages that also rely on the library. For an example, see +[this Haml file][page-specific-js-example]. + +### Minimizing page size + +A smaller page size means the page loads faster (especially important on mobile +and poor connections), the page is parsed more quickly by the browser, and less +data is used for users with capped data plans. + +General tips: + +- Don't add new fonts. +- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF. +- Compress and minify assets wherever possible (For CSS/JS, Sprockets does this for us). +- If some functionality can reasonably be achieved without adding extra libraries, avoid them. +- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages. + +## Accessibility + +### Resources + +[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools] +are useful for testing for potential accessibility problems in GitLab. + +Accessibility best-practices and more in-depth information is available on +[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools. + +## Security + +### Resources + +[Mozilla’s HTTP Observatory CLI][observatory-cli] and the +[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding +potential problems and ensuring compliance with security best practices. + +<!-- Uncomment these sections when CSP/SRI are implemented. +### Content Security Policy (CSP) + +Content Security Policy is a web standard that intends to mitigate certain +forms of Cross-Site Scripting (XSS) as well as data injection. + +Content Security Policy rules should be taken into consideration when +implementing new features, especially those that may rely on connection with +external services. + +GitLab's CSP is used for the following: + +- Blocking plugins like Flash and Silverlight from running at all on our pages. +- Blocking the use of scripts and stylesheets downloaded from external sources. +- Upgrading `http` requests to `https` when possible. +- Preventing `iframe` elements from loading in most contexts. + +Some exceptions include: + +- Scripts from Google Analytics and Piwik if either is enabled. +- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing. +- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication. + +We use [the Secure Headers gem][secure_headers] to enable Content +Security Policy headers in the GitLab Rails app. + +Some resources on implementing Content Security Policy: + +- [MDN Article on CSP][mdn-csp] +- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp] +- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4] + +### Subresource Integrity (SRI) + +Subresource Integrity prevents malicious assets from being provided by a CDN by +guaranteeing that the asset downloaded is identical to the asset the server +is expecting. + +The Rails app generates a unique hash of the asset, which is used as the +asset's `integrity` attribute. The browser generates the hash of the asset +on-load and will reject the asset if the hashes do not match. + +All CSS and JavaScript assets should use Subresource Integrity. For implementation details, +see the documentation for [the Sprockets implementation of SRI][sprockets-sri]. + +Some resources on implementing Subresource Integrity: + +- [MDN Article on SRI][mdn-sri] +- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri] + +--> + +### Including external resources + +External fonts, CSS, and JavaScript should never be used with the exception of +Google Analytics and Piwik - and only when the instance has enabled it. Assets +should always be hosted and served locally from the GitLab instance. Embedded +resources via `iframes` should never be used except in certain circumstances +such as with ReCaptcha, which cannot be used without an `iframe`. + +### Avoiding inline scripts and styles + +In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy. + +While inline scripts can be useful, they're also a security concern. If +user-supplied content is unintentionally left un-sanitized, malicious users can +inject scripts into the web app. + +Inline styles should be avoided in almost all cases, they should only be used +when no alternatives can be found. This allows reusability of styles as well as +readability. + +## Style guides and linting + +See the relevant style guides for our guidelines and for information on linting: + +- [SCSS][scss-style-guide] + +## Testing + +Feature tests need to be written for all new features. Regression tests +also need to be written for all bug fixes to prevent them from occurring +again in the future. + +See [the Testing Standards and Style Guidelines](testing.md) for more +information. + +## Supported browsers + +For our currently-supported browsers, see our [requirements][requirements]. + +[rails]: http://rubyonrails.org/ +[haml]: http://haml.info/ +[hamlit]: https://github.com/k0kubun/hamlit +[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations +[scss]: http://sass-lang.com/ +[es6]: https://babeljs.io/ +[sprockets]: https://github.com/rails/sprockets +[jquery]: https://jquery.com/ +[vue]: http://vuejs.org/ +[vue-docs]: http://vuejs.org/guide/index.html +[web-page-test]: http://www.webpagetest.org/ +[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ +[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en +[browser-diet]: https://browserdiet.com/ +[d3]: https://d3js.org/ +[chartjs]: http://www.chartjs.org/ +[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 +[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools +[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules +[observatory-cli]: https://github.com/mozilla/http-observatory-cli) +[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html +[secure_headers]: https://github.com/twitter/secureheaders +[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP +[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/ +[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/ +[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ +[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/ +[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/ +[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity +[github-eng-sri]: http://githubengineering.com/subresource-integrity/ +[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support +[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting +[scss-style-guide]: scss_styleguide.md +[requirements]: ../install/requirements.md#supported-web-browsers diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 295eae0a88e..61b0fbc89c9 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -9,10 +9,10 @@ a big burden for most organizations. For this reason it is important that your migrations are written carefully, can be applied online and adhere to the style guide below. Migrations should not require GitLab installations to be taken offline unless -_absolutely_ necessary. If a migration requires downtime this should be -clearly mentioned during the review process as well as being documented in the -monthly release post. For more information see the "Downtime Tagging" section -below. +_absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md) +page. If a migration requires downtime, this should be clearly mentioned during +the review process, as well as being documented in the monthly release post. For +more information, see the "Downtime Tagging" section below. When writing your migrations, also consider that databases might have stale data or inconsistencies and guard for that. Try to make as little assumptions as possible diff --git a/doc/install/installation.md b/doc/install/installation.md index 3ac813aa914..68ed20ef5bf 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future. +**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future. The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab in production, frequently leads to hard to diagnose problems. For example, @@ -268,9 +268,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-12-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-13-stable gitlab -**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -381,7 +381,7 @@ sudo usermod -aG redis git GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: diff --git a/doc/university/README.md b/doc/university/README.md new file mode 100644 index 00000000000..6ca1c20c9b2 --- /dev/null +++ b/doc/university/README.md @@ -0,0 +1,139 @@ + +## What is GitLab University + +_GitLab University_ has as a goal to teach the fundamentals of **Version Control with Git and GitLab** through courses that cover topics which can be mastered in around 2 hours. + +_University materials don't replace our [Documentation](http://docs.gitlab.com) or [Blog Articles](https://about.gitlab.com/blog/)._ + +--- + +### On this page + ++ [GITx] Git ++ [OPSx] DevOps ++ [GLBx] GitLab Basics ++ [INTx] GitLab Integrations ++ [GLFx] GitLab Workflows ++ [GLEx] GitLab Enterprise Edition extra features ++ [GCIx] GitLab CI ++ [ECO] Ecosystem ++ [COM] Competition comparison ++ [SPTx] Support Bootcamp ++ [SLSx] Sales Bootcamp ++ [TRAx] Trainings + +--- + ++ [GIT1] [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29) ++ [GIT2] [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing) ++ [GIT3] [Intro to Git](https://www.codeschool.com/account/courses/try-git) + +--- + ++ [OPS1] [What is Omnibus](https://www.youtube.com/watch?v=XTmpKudd-Oo) ++ [OPS2] [Installing GitLab](https://www.youtube.com/watch?v=Q69YaOjqNhg) ++ [OPS3] [Configuring an external PostgreSQL database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server) ++ [OPS5] [Importing from Other Tools or SVN](http://doc.gitlab.com/ee/workflow/importing/) ++ [OPS6] [High Availability Documentation](https://about.gitlab.com/high-availability/) ++ [OPS7] [Managing LDAP, Active Directory](https://www.youtube.com/watch?v=HPMjM-14qa8) ++ [OPS8] [Scalability and High Availability](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2) ++ [OPS9] [High Availability on AWS](high-availability/aws/README.md) + +--- + ++ [GLB1] [Terminology](glossary/README.md) ++ [GLB2] [GitLab Basics](http://doc.gitlab.com/ce/gitlab-basics/README.html) ++ [GLB3] [Demo of GitLab.com](https://www.youtube.com/watch?v=WaiL5DGEMR4) ++ [GLB4] [Create and Add your SSH key to GitLab](https://www.youtube.com/watch?v=54mxyLo3Mqk) ++ [GLB5] [Repositories, Projects and Groups](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e) ++ [GLB6] [Creating a Project in GitLab](https://www.youtube.com/watch?v=7p0hrpNaJ14) ++ [GLB7] [Issues and Merge Requests](https://www.youtube.com/watch?v=raXvuwet78M) ++ [GLB8] [Big files in Git (Git LFS, Annex)](https://gitlab.com/gitlab-org/University/blob/master/classes/git_lfs_and_annex.md) + +--- + ++ [INT1] [JIRA and Jenkins integrations in GitLab](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415) ++ [INT2] [Integrating JIRA with GitLab](http://doc.gitlab.com/ee/integration/jira.html) ++ [INT3] [Integrating Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html) ++ [INT4] [Integrating Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md) ++ [INT5] [Documentation on Integrating Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md) + +--- + ++ [GLF1] [GitLab Flow](https://www.youtube.com/watch?v=UGotqAUACZA) + +--- + ++ [GLE1] [Configuring an external MySQL database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only) ++ [GLE2] [Managing Permissions within EE](https://www.youtube.com/watch?v=DjUoIrkiNuM) ++ [GLE3] [Upcoming in EE and Big files in Git (Git LFS, Annex)](https://gitlab.com/gitlab-org/University/blob/master/classes/upcoming_in_ee.md) + +--- + ++ [GCI1] [GitLab CI product page](https://about.gitlab.com/gitlab-ci/) ++ [GCI2] [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/) + +--- + ++ [COM1] [GitLab compared to other tools](https://about.gitlab.com/comparison/) ++ [COM2] [Compare GitLab versions](https://about.gitlab.com/features/#compare) ++ [COM3] [Innersourcing article](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/) + +--- + ++ [ECO1] [Ecosystem Overview](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) ++ [ECO2] [Positioning FAQ](https://about.gitlab.com/handbook/positioning-faq) ++ [ECO3] [GitLab Ecosystem slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit) ++ [ECO4] [Customer Use-Cases](https://about.gitlab.com/handbook/use-cases/) + +--- + ++ [SPT1] [Support Path](support/README.md) ++ [SPT2] [End User Training Material](https://gitlab.com/gitlab-org/University/blob/master/training/user_training.md) ++ [SPT3] [Materials for Training Sessions](https://gitlab.com/gitlab-org/University/tree/master/training/topics) + +--- + ++ [SLS1] [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/) ++ [SLS2] [GitLab Direction](https://about.gitlab.com/direction/) + +--- + ++ [TRA1] [End User Training](training/end-user/README.md) + +--- + +### External Resources + ++ [DOC] GitLab Documentation + + [Set up and use GitLab Pages](http://doc.gitlab.com/ee/pages/README.html) + + [Markdown Reference](http://doc.gitlab.com/ce/markdown/markdown.html) + ++ [GLW] GitLab Workshop (@ Platzi) + + [GitLab Workshop Part 1: Basics of Git and GitLab](https://courses.platzi.com/classes/git-gitlab/) + + [Create a GitLab Account](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/) + ++ [GLY] GitLab YouTube Videos + + [Making GitLab Great for Everyone, our response to the Dear GitHub letter](https://www.youtube.com/watch?v=GGC40y4vMx0) + + [Compared to Atlassian (Recorded on 2016-03-03) ](https://youtu.be/Nbzp1t45ERo) + ++ [GLI] GitLab Team-Only Access + + [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md) + + [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing) + ++ [KNT] Slides & Keynotes by GitLabbers & other individuals + + [Why Git and GitLab slide deck](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/) + + [Git Workshop](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/) + ++ Others (not created by GitLab) + + [Dev Ops terminology](https://xebialabs.com/glossary/) + + [Continuous Delivery vs Continuous Deployment](https://www.youtube.com/watch?v=igwFj8PPSnw) + + [Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/) + + [State of Dev Ops 2015 Report by Puppet Labs](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf) Insightful Chapters to understand the Impact of Continuous Delivery on Performance (Chapter 4), the Application Architecture (Chapter 5) and How IT Managers can help their teams win (Chapter 6). + + [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) + + [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/) + + [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/) + + [Customer review of GitLab with talking points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/) + + [3rd party tool comparison](http://technologyconversations.com/2015/10/16/github-vs-gitlabs-vs-bitbucket-server-formerly-stash/) + + [Amazon's transition to Continuous Delivery](https://www.youtube.com/watch?v=esEFaY0FDKc) + + [Article on Continuous Integration from ThoughtWorks](https://www.thoughtworks.com/continuous-integration) diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md new file mode 100644 index 00000000000..a86ff165f2e --- /dev/null +++ b/doc/university/glossary/README.md @@ -0,0 +1,482 @@ + +## What is the Glossary + +This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab. +Please add any terms that you discover that you think would be useful for others. + +### 2FA + +User authentication by combination of 2 different steps during login. This allows for more security. + +### Access Levels + +Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. +See, [GitLab's Permission Guidelines](http://doc.gitlab.com/ce/permissions/permissions.html) + +### Active Directory (AD) + +A Microsoft based directory service for windows domain networks. It uses LDAP technology under the hood + +### Agile + +Building and delivering software in phases/parts rather than trying to build everything at once then delivering to the user/client. The later is known as a WaterFall model + +### Application Lifecycle Management (ALM) + +Entire product lifecycle management process for an application. From requirements management, development and testing until deployment. + +### Artifactory + +Version control for binaries. + +### Artifacts + +objects (usually binary and large) created by a build process + +### Atlassian + +A company that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo. See [Atlassian] (https://www.atlassian.com) + +### Audit Log + +*** Needs definition here + +### Auto Defined User Group + +User groups are a way of centralizing control over important management tasks, particularly access control and password policies. +A simple example of such groups are the users and the admins groups. +In most of the cases these groups are auto defined in terms of access, rules of usage, conditions to be part of, etc... + +### Bamboo + +Atlassian's CI tool similar to GitLab CI and Jenkins + +### Basic Subscription + +Entry level subscription for GitLab EE currently available in packs of 10 see [Basic subscription](https://about.gitlab.com/pricing/) + +### Bitbucket + +Atlassian's web hosting service for Git and Mercurial Projects i.e. GitLab.com competitor + +### Branch + +A branch is a parallel version of a repository. Allows you to work on the repository without you affecting the "master" branch. Allows you to make changes without affecting the current "live" version. When you have made all your changes to your branch you can then merge to the master and to make the changes fo "live". + +### Branded Login + +Having your own logo on your GitLab instance login page instead of the GitLab logo. + +### CEPH + +is a distributed object store and file system designed to provide excellent performance, reliability and scalability. + +### Clone + +A copy of a repository stored on your machine that allows you to use your own editor without being online, but still tracks the changes made remotely. + +### Code Review + +Examination of a progam's code. The main aim is to maintain high standards quality of code that is being shipped. + +### Code Snippet + +A small amount of code. Usually for the purpose of showing other developers how +to do something specific or reproduce a problem. + +### Collaborator + +Person with read and write access to a repository who has been invited by repository owner. + +### Commit + +Is a change (revision) to a file, and also creates an ID that allows you to see revision history and who made the changes. + +### Community + +Everyone who is using GitLab + +### Confluence + +Atlassian's product for collaboration of documents and projects. + +### Continuous Deivery + +Continuous delivery is a series of practices designed to ensure that code can be rapidly and safely deployed to production by delivering every change to a production-like environment and ensuring business applications and services function as expected through rigorous automated testing. + +### Continuous Deployment + +Continuous deployment is the next step of continuous delivery: Every change that passes the automated tests is deployed to production automatically. + +### Continuous Integration + +A process that involves adding new code commits to source code with the combined code being run on an automated test to ensure that the changes do not break the software. + +### Contributor + +Term used to a person contributing to an Open Source Project. + +### Data Centre + +Atlassian product for High Availability. + +### Deploy Keys + +An SSH key stored on the your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code. + +### Developer + +For us (GitLab) this means a software developer, i.e. someone who makes software. It is also one of the levels of access in our multi level approval system. + +### Diff + +Is the difference between two commits, or saved changes. This will also be shown visually after the changes. + +### Docker + +Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. +This guarantees that it will always run the same, regardless of the environment it is running in. + +### Fork + +Your own copy of a repository that allows you to make changes to the repository without affecting the original. + +### Gerrit + +A code review tool built on top of Git. + +### Git Hooks + +Are scripts you can use to trigger actions at certain points. + +### GitHost.io + +Is a single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for +installing, updating, hosting, and backing up customers own private and secure GitLab instance. + +### GitHub + +A web-based Git repository hosting service with an enterprise offering. Its main features are: issue tracking, pull request with code review, abundancy of integrations and wiki. As of April 2016, the service has over 14 million users. It offers free public repos, private repos and enterprise services are paid. + +### GitLab CE + +Our free on Premise solution with >100,000 users + +### GitLab CI + +Our own Continuos Integration feature that is shipped with each instance + +### GitLab EE + +Our premium on premise solution that currently has Basic, Standard and Plus subscription packages with additional features and support. + +### GitLab.com + +Our free SaaS for public and private repositories. + +### Gitolite + +Is basically an access layer that sits on top of Git. Users are granted access to repos via a simple config file and you as an admin only needs the users public SSH key and a username from the user. + +### Gitorious + +A web based hosting service for projects using Git. It was acquired by GitLab and we discontinued the service. [Gitorious Acquisition Blog Post](https://about.gitlab.com/2015/03/03/gitlab-acquires-gitorious/) + +### HADR + +Sometimes written HA/DR. High Availability for Disaster Recovery. Usually refers to a strategy having a failover server in place in case the main server fails. + +### Hip Chat + +Atlassian's real time chat application for teams. Competitor to Slack, RocketChat and MatterMost. + +### High Availability + +Refers to a system or component that is continuously operational for a desirably long length of time. Availability can be measured relative to "100% operational" or "never failing." + +### Issue Tracker + +A tool used to manage, organize, and maintain a list of issues, making it easier for an organization to manage. + +### Jenkins + +An Open Source CI tool written using the Java programming language. Does the same job as GitLab CI, Bamboo, Travis CI. It is extremely popular. see [Jenkins](https://jenkins-ci.org/) + +### Jira + +Atlassian's project management software. i.e. a complex issue tracker. See[Jira](https://www.atlassian.com/software/jira) + +### Kerberos + +A network authentication protocol that uses secret-key cryptography for security. + +### Kubernetes + +An open source container cluster manager originally designed by Google. It's basically a platform for automating deployment, scaling, and operations of application containers over clusters of hosts. + +### Labels + +An identifier to describe a group of one or more specific file revisions + +### LDAP + +Lightweight Directory Access Protocol - basically its a directory (electronic address book) with user information e.g. name, phone_number etc + +### LDAP User Authentication + +Allowing GitLab to sign in people from an LDAP server i.e. Allow people whose names are on the electronic user directory server) to be able to use their LDAP accounts to login. + +### LDAP Group Sync + +Allows you to synchronize the members of a GitLab group with one or more LDAP groups. + +### Git LFS + +Git Large File Storage. A way to enable git to handle large binary files by using reference pointers within small text files to point to the large files. + +### Linux + +An operating system like Windows or OS X. It is mostly used by software developers and on servers. + +### Markdown + +Is a lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML and many other formats using a tool by the same name. +Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. + +### Maria DB + +A community developed fork/variation of MySQL. MySQL is owned by Oracle. + +### Master + +Name of the default branch in every git repository. + +### Mercurial + +A free distributed version control system like Git. Think of it as a competitor to Git. + +### Merge + +Takes changes from one branch, and applies them into another branch. + +### Meteor + +A hip platform for building javascript apps.[Meteor] (https://www.meteor.com) + +### Milestones + +Allows you to track the progress on issues, and merge requests, which allows you to get a snapshot of the progress made. + +### Mirror Repositories + +You can set up a project to automatically have its branches, tags, and commits updated from an upstream repository. This is useful when a repository you're interested in is located on a different server, and you want to be able to browse its content and its activity using the familiar GitLab interface. + +### MIT License + +A type of software license. It lets people do anything with your code with proper attribution and without warranty. It is the most common license for open source applications written in Ruby on Rails. GitLab CE is issued under this license. +This means, you can download the code, modify it as you want even build a new commercial product using the underlying code and its not illegal. The only condition is that there is no form of waranty provided by GitLab so whatever happens if you use the code is your own problem. + +### Mondo + +*** Needs definition here + +### Multi LDAP Server + +*** Needs definition here + +### My SQL + +A relational database. Currently only supported if you are using EE. It is owned by Oracle. + +### Namespace + +In computing, a namespace is a set of symbols that are used to organize objects of various kinds, so that these objects may be referred to by name. + +Prominent examples include: +- file systems are namespaces that assign names to files; +- programming languages organize their variables and subroutines in namespaces; +- computer networks and distributed systems assign names to resources, such as computers, printers, websites, (remote) files, etc. + +### Nginx + +(pronounced "engine x") is a web server. It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache. + +### oAuth + +Is an open standard for authorization, commonly used as a way for Internet users to log into third party websites using their Microsoft, Google, Facebook or Twitter accounts without exposing their password. + +### Omnibus Packages + +Omnibus is a way to package the different services and tools required to run GitLab, so that users can install it without as much work. + +### On Premise + +On your own server. In GitLab, this refers to the ability to download GitLab EE/GitLab CE and host it on your own server rather than using GitLab.com which is hosted by GitLab Inc's servers. + +### Open Source Software + +Software for which the original source code is freely available and may be redistributed and modified. + +### Owner + +This is the most powerful person on a GitLab project. He has the permissions of all the other users plus the additional permission of being able to destroy i.e. delete the project + +### PaaS + +Typically referred to in regards to application development, it is a model in which a cloud provider delivers hardware and software tools to its users as a service + +### Perforce + +The company that produces Helix. A commercial, proprietary, centralised VCS well known for it's ability to version files of any size and type. They OEM a re-branded version of GitLab called "GitSwarm" that is tightly integrated with their "GitFusion" product, which in turn represents a portion of a Helix repository (called a depot) as a git repo + +### Phabricator + +Is a suite of web-based software development collaboration tools, including the Differential code review tool, the Diffusion repository browser, the Herald change monitoring tool, the Maniphest bug tracker and the Phriction wiki. Phabricator integrates with Git, Mercurial, and Subversion. + +### Piwik Analytics + +An open source analytics software to help you analyze web traffic. It is similar to google analytics only that google analytics is not open source and information is stored by google while in Piwik the information is stored in your own server hence fully private. + +### Plus Subscription + +GitLab Premium EE subscription that includes training and dedicated Account Management and Service Engineer and complete support package [Plus subscription](https://about.gitlab.com/pricing/) + +### PostgreSQL + +A relational database. Touted as the most advanced open source database. + +### Protected Branches + +A feature that protects branches from unauthorized pushes, force pushing or deletion. + +### Pull + +Git command to synchronize the local repository with the remote repository, by fetching all remote changes and merging them into the local repository. + +### Puppet + +A popular devops automation tool + +### Push + +Git command to send commits from the local repository to the remote repository. + +### RE Read Only + +Permissions to see a file and it's contents, but not change it + +### Rebase + +Moves a branch from one commit to another. This allows you to re-write your project's history. + +### Git Repository + +Storage location of all files which are tracked by git. + +### Requirements management + +*** Needs definition here + +### Revision + +*** Needs definition here + +### Revision Control + +Also known as version control or source control, is the management of changes to documents, computer programs, large web sites, and other collections of information. Changes are usually identified by a number or letter code, termed the "revision number", "revision level", or simply "revision". + +### RocketChat + +An open source chat application for teams. Very similar to Slack only that is is open-source. + +### Runners + +Actual build machines/containers that run/execute tests you have specified to be run on GitLab CI + +### SaaS + +Software as a service. Software is hosted centrally and accessed on-demand i.e. when you want to. This refers to GitLab.com in our scenario + +### SCM + +Software Configuration Management. Often used by people when they mean Version Control + +## Scrum + +An Agile framework designed to help complete complex (typically) software projects. It's made up of several parts: product requirments backlog, sprint plannnig, sprint (development), sprint review, retrospec (analyzing the sprint). The goal is to end up with potentially shippable products. + +### Scrum Board + +The board used to track the status and progress of each of the sprint backlog items. + +### Slack + +Real time messaging app for teams. Used internally by GitLab + +### Slave Servers + +Also known as secondary servers. They help to spread the load over multiple machines, they also provide backups when the master/primary server crashes. + +### Source Code + +Program code as typed by a computer programmer. i.e. it has not yet been compiled/translated by the computer to machine language. + +### SSH Key + +A unique identifier of a computer. It is used to identify computers without the need for a password. e.g. On GitLab I have added the ssh key of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added. + +### SSO + +Single Sign On. An authentication process that allows you enter one username and password to access multiple applications. + +### Standard Subscription + +Our mid range EE subscription that includes 24/7 support, support for High Availability [Standard Subscription](https://about.gitlab.com/pricing/) + +### Stash + +Atlassian's Git On-Premises solution. Think of it as Atlassian's GitLab EE. It is now known as BitBucket Server. + +### Subversion + +Non-proprietary, centralized version control system. + +### Sudo + +A program that allows you to perform superuser/administrator actions on Unix Operating Systems e.g. Linux, OS X. It actually stands for 'superuser do' + +### SVN + +Abbreviation for Subversion. + +### Tag + +Represents a version of a particular branch at a moment in time. + +### Tool Stack + +Set of tools used in a process to achieve a common outcome. E.g. set of tools used in Application Lifecycle Management. + +### Trac + +An Open Source project management and bug tracking web application. + +### User + +Anyone interacting with the software. + +### VCS + +Version Control Software + +### Waterfall + +A model of building software that involves collecting all requirements from the customer, then building and refining all the requirements and finally delivering the COMPLETE software to the customer that meets all the requirements specified by the customer + +### Webhooks + +A way for for an app to provide other applications with real-time information. e.g. send a message to a slack channel when a commit is pushed + +### Wiki + +A website/system that allows for collaborative editing of its content by the users. In programming, they usually contain documentation of how to use the software diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md new file mode 100644 index 00000000000..088f1cd7290 --- /dev/null +++ b/doc/university/high-availability/aws/README.md @@ -0,0 +1,387 @@ + +# High Availability on AWS + +GitLab on AWS can leverage many of the services that are already +configurable with High Availability. These services have a lot of +flexibility and are able to adopt to most companies, best of all is the +ability to automate both vertical and horizontal scaling. + +In this article we'll go through a basic HA setup where we'll start by +configuring our Virtual Private Cloud and subnets to later integrate +services such as RDS for our database server and ElastiCache as a Redis +cluster to finally manage them within an auto scaling group with custom +scaling policies. + +*** + +## Where to Start + +Login to your AWS account through the `My Account` dropdown on +`https://aws.amazon.com` or through the URI assigned to your team such as +`https://myteam.signin.aws.amazon.com/console/`. You'll start on the +Amazon Web Services console from where we can choose all of the services +we'll be using to configure our cloud infrastructure. + +*** + +## Network + +We'll start by creating a VPC for our GitLab cloud infrastructure, then +we can create subnets to have public and private instances in at least +two AZs. Public subnets will require a Route Table keep an associated +Internet Gateway. + +### VPC + +Start by looking for the VPC option on the web console. Now create a new +VPC. We can use `10.0.0.0/16` for the CIDR block and leave tenancy as +default if we don't require dedicated hardware. + +![New VPC](img/new_vpc.png) + +If you're setting up the Elastic File System service then select the VPC +and from the Actions dropdown choose Edit DNS Hostnames and select Yes. + +### Subnet + +Now let's create some subnets in different Availability Zones. Make sure +that each subnet is associated the the VPC we just created, that it has +a distinct VPC and lastly that CIDR blocks don't overlap. This will also +allow us to enable multi AZ for redundancy. + +We will create private and public subnets to match load balancers and +RDS instances as well. + +![Subnet Creation](img/subnet.png) + +The subnets are listed with their name, AZ and CIDR block: + +* gitlab-public-10.0.0.0 - us-west-2a - 10.0.0.0 +* gitlab-private-10.0.1.0 - us-west-2a - 10.0.1.0 +* gitlab-public-10.0.2.0 - us-west-2b - 10.0.2.0 +* gitlab-private-10.0.3.0 - us-west-2b - 10.0.3.0 + +### Route Table + +Up to now all our subnets are private. We need to create a Route Table +to associate an Internet Gateway. On the same VPC dashboard choose +Route Tables on the left column and give it a name and associate it to +our newly created VPC. + +![Route Table](img/route_table.png) + + +### Internet Gateway + +Now still on the same dashboard head over to Internet Gateways and +create a new one. After its created pres on the `Attach to VPC` button and +select our VPC. + +![Internet Gateway](img/ig.png) + +### Configure Subnets + +Go back to the Router Tables screen and select the newly created one, +press the Routes tab on the bottom section and edit it. We need to add a +new target which will be our Internet Gateway and have it receive +traffic from any destination. + +![Subnet Config](img/ig-rt.png) + +Before leaving this screen select the next tab to the rgiht which is +Subnet Associations and add our public subnets. If you followed our +naming convention they should be easy to find. + +*** + +## Database with RDS + +For our database server we will use Amazon RDS which offers Multi AZ +for redundancy. Lets start by creating a subnet group and then we'll +create the actual RDS instance. + +### Subnet Group + +From the RDS dashboard select Subnet Groups. Lets select our VPC from +the VPC ID dropdown and at the bottom we can add our private subnets. + +![Subnet Group](img/db-subnet-group.png) + +### RDS + +Select the RDS service from the Database section and create a new +PostgreSQL instance. After choosing between a Production or +Development instance we'll start with the actual configuration. On the +image bellow we have the settings for this article but note the +following two options which are of particular interest for HA: + +1. Multi-AZ-Deployment is recommended as redundancy. Read more at +[High Availability (Multi-AZ)](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html) +1. While we chose a General Purpose (SSD) for this article a Provisioned +IOPS (SSD) is best suited for HA. Read more about it at +[Storage for Amazon RDS](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html) + +![RDS Instance Specs](img/instance_specs.png) + +The rest of the setting on this page request a DB identifier, username +and a master password. We've chosen to use `gitlab-ha`, `gitlab` and a +very secure password respectively. Keep these in hand for later. + +![Network and Security](img/rds-net-opt.png) + +Make sure to choose our gitlab VPC, our subnet group, not have it public, +and to leave it to create a new security group. The only additional +change which will be helpful is the database name for which we can use +`gitlabhq_production`. + +*** + +## ElastiCache + +EC is an in-memory hosted caching solution. Redis maintains its own +persistance and is used for certain types of application. + +Let's choose the ElastiCache service in the Database section from our +AWS console. Now lets create a cache subnet group which will be very +similar to the RDS subnet group. Make sure to select our VPC and its +private subnets. + +![ElastiCache](img/ec-subnet.png) + +Now press the Launch a Cache Cluster and choose Redis for our +DB engine. You'll be able to configure details such as replication, +Multi AZ and node types. The second section will allow us to choose our +subnet and security group and + +![Redis Cluster details](img/redis-cluster-det.png) + +![Redis Network](img/redis-net.png) + +*** + +## Elastic File System + +This new AWS offering allows us to create a file system accessible by
 +EC2 instances within a VPC. Choose our VPC and the subnets will be +
automatically configured assuming we don't need to set explicit IPs. +The
next section allows us to add tags and choose between General +Purpose or
Max I/O which is a good option when being accessed by a +large number of
EC2 instances. + +

![Elastic File System](img/elastic-file-system.png) + +To actually mount and install the NFS client we'll use the User Data +section when adding our Launch Configuration. + +*** + +## Initiate AMI + +We are going to launch an EC2 instance and bake an image so that we can +later use it for auto scaling. We'll also take this opportunity to add an +extension to our RDS through this temporary EC2 instance. + +### EC2 Instance + +Look for the EC2 option and choose to create an instance. We'll need at +least a t2.medium type and for this article we'll choose an Ubuntu 14.04 +HVM 64-bit. In the Configure Instance section choose our GitLab VPC and +a public subnet. I'd choose at least 10GB of storage. + +In the security group we'll create a new one considering that we need to +SSH into the instance and also try it out through http. So let's add the +http traffic from anywhere and name it something such as +`gitlab-ec2-security-group`. + +While we wait for it to launch we can allocate an Elastic IP and +associate it with our new EC2 instance. + +### RDS and Redis Security Group + +After the instance is being created we will navigate to our EC2 security +groups and add a small change for our EC2 instances to be able to +connect to RDS. First copy the security group name we just defined, +namely `gitlab-ec2-security-group`, and edit select the RDS security +group and edit the inbound rules. Choose the rule type to be PostgreSQL +and paste the name under source. + +![RDS security group](img/rds-sec-group.png) + +Similar to the above we'll jump to the `gitlab-ec2-security-group` group +and add a custom TCP rule for port 6379 accessible within itself. + +### Install GitLab + +To connect through SSH you will need to have the `pem` file which you +chose available and with the correct permissions such as `400`. + +After accessing your server don't forget to update and upgrade your +packages. + + sudo apt-get update && sudo apt-get upgrade -y + +Then follow installation instructions from +[GitLab](https://about.gitlab.com/downloads-ee/#ubuntu1404), but before +running reconfigure we need to make sure all our services are tied down +so just leave the reconfigure command until after we edit our gitlab.rb +file. + + +### Extension for PostgreSQL + +Connect to your new RDS instance to verify access and to install +a required extension. We can find the host or endpoint by selecting the +instance and we just created and after the details drop down we'll find +it labeled as 'Endpoint'; do remember not to include the colon and port +number. + + sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production + psql (9.4.7) + Type "help" for help. + + gitlab=# CREATE EXTENSION pg_trgm; + gitlab=# \q + +### Configure GitLab + +While connected to your server edit the `gitlab.rb` file at `/etc/gitlab/gitlab.rb` +find the `external_url 'http://gitlab.example.com'` option and change it +to the domain you will be using or the public IP address of the current +instance to test the configuration. + +For a more detailed description about configuring GitLab read [Configuring GitLab for HA](http://docs.gitlab.com/ee/administration/high_availability/gitlab.html) + +Now look for the GitLab database settings and uncomment as necessary. In +our current case we'll specify the adapter, encoding, host, db name, +username, and password. + + gitlab_rails['db_adapter'] = "postgresql" + gitlab_rails['db_encoding'] = "unicode" + gitlab_rails['db_database'] = "gitlabhq_production" + gitlab_rails['db_username'] = "gitlab" + gitlab_rails['db_password'] = "mypassword" + gitlab_rails['db_host'] = "<rds-endpoint>" + +Next we only need to configure the Redis section by adding the host and +uncommenting the port. + + + +The last configuration step is to [change the default file locations ](http://docs.gitlab.com/ee/administration/high_availability/nfs.html) +to make the EFS integration easier to manage. + + gitlab_rails['redis_host'] = "<redis-endpoint>" + gitlab_rails['redis_port'] = 6379 + +Finally run reconfigure, you might find it useful to run a check and +a service status to make sure everything has been setup correctly. + + sudo gitlab-ctl reconfigure + sudo gitlab-rake gitlab:check + sudo gitlab-ctl status + +If everything looks good copy the Elastic IP over to your browser and +test the instance manually. + +### AMI + +After you finish testing your EC2 instance go back to its dashboard and +while the instance is selected press on the Actions dropdown to choose +Image -> Create an Image. Give it a name and description and confirm. + +*** + +## Load Balancer + +On the same dashboard look for Load Balancer on the left column and press +the Create button. Choose a classic Load Balancer, our gitlab VPC, not +internal and make sure its listening for HTTP and HTTPS on port 80. + +Here is a tricky part though, when adding subnets we need to associate +public subnets instead of the private ones where our instances will +actually live. + +On the secruity group section let's create a new one named +`gitlab-loadbalancer-sec-group` and allow both HTTP ad HTTPS traffic +from anywhere. + +The Load Balancer Health will allow us to indicate where to ping and what +makes up a healthy or unhealthy instance. + +We won't add the instance on the next session because we'll destroy it +momentarily as we'll be using the image we where creating. We will keep +the Enable Cross-Zone and Enable Connection Draining active. + +After we finish creating the Load Balancer we can re visit our Security +Groups to improve access only through the ELB and any other requirement +you might have. + +*** + +## Auto Scaling Group + +Our AMI should be done by now so we can start working on our Auto +Scaling Group. + +This option is also available through the EC2 dashboard on the left +sidebar. Press on the create button. Select the new image on My AMIs and +give it a `t2.medium` size. To be able to use Elastic File System we need +to add a script to mount EFS automatically at launch. We'll do this at +the Advanced Details section where we have a [User Data](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) +text area that allows us to add a lot of custom configurations which +allows you to add a custom script for when launching an instance. Let's +add the following script to the User Data section: + + + #cloud-config + package_upgrade: true + packages: + - nfs-common + runcmd: + - mkdir -p /gitlab-data + - chown ec2-user:ec2-user /gitlab-data + - echo "$(curl --silent http://169.254.169.254/latest/meta-data/placement/availability-zone).file-system-id.aws-region.amazonaws.com:/ /gitlab-data nfs defaults,vers=4.1 0 0" >> /etc/fstab + - mount -a -t nfs + - sudo gitlab-ctl reconfigure + +On the security group section we can chosse our existing +`gitlab-ec2-security-group` group which has already been tested. + +After this is launched we are able to start creating our Auto Scaling +Group. Start by giving it a name and assinging it our VPC and private +subnets. We also want to always start with two instances and if you +scroll down to Advanced Details we can choose to receive traffic from ELBs. +Lets enable that option and select our ELB. We also want to use the ELB's +health check. + +![Auto scaling](img/auto-scaling-det.png) + +### Policies + +This is the really great part of Auto Scaling, we get to choose when AWS +launches new instances and when it removes them. For this group we'll +scale between 2 and 4 instances where one instance will be added if CPU +utilization is greater than 60% and one instance is removed if it falls +to less than 45%. Here are the complete policies: + +![Policies](img/policies.png) + +You'll notice that after we save this AWS starts launching our two +instances in different AZs and without a public IP which is exactly what +we where aiming for. + +*** + +## Final Thoughts + +After you're done with the policies section have some fun trying to break +instances. You should be able to see how the Auto Scaling Group and the +EC2 screen start bringing them up again. + +High Availability is a very big area, we went mostly through scaling and +some redundancy options but it might also imply Geographic replication. +There is a lot of ground yet to cover so have a read through these other +resources and feel free to open an issue to request additional material. + + * [GitLab High Availability](http://docs.gitlab.com/ce/administration/high_availability/README.html#sts=High Availability) + * [GitLab Geo](http://docs.gitlab.com/ee/gitlab-geo/README.html) diff --git a/doc/university/high-availability/aws/img/auto-scaling-det.png b/doc/university/high-availability/aws/img/auto-scaling-det.png Binary files differnew file mode 100644 index 00000000000..e9b65529495 --- /dev/null +++ b/doc/university/high-availability/aws/img/auto-scaling-det.png diff --git a/doc/university/high-availability/aws/img/db-subnet-group.png b/doc/university/high-availability/aws/img/db-subnet-group.png Binary files differnew file mode 100644 index 00000000000..0768aa73c45 --- /dev/null +++ b/doc/university/high-availability/aws/img/db-subnet-group.png diff --git a/doc/university/high-availability/aws/img/ec-subnet.png b/doc/university/high-availability/aws/img/ec-subnet.png Binary files differnew file mode 100644 index 00000000000..f41d78b271d --- /dev/null +++ b/doc/university/high-availability/aws/img/ec-subnet.png diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png Binary files differnew file mode 100644 index 00000000000..7de866d1e89 --- /dev/null +++ b/doc/university/high-availability/aws/img/elastic-file-system.png diff --git a/doc/university/high-availability/aws/img/ig-rt.png b/doc/university/high-availability/aws/img/ig-rt.png Binary files differnew file mode 100644 index 00000000000..93bb0c2ae02 --- /dev/null +++ b/doc/university/high-availability/aws/img/ig-rt.png diff --git a/doc/university/high-availability/aws/img/ig.png b/doc/university/high-availability/aws/img/ig.png Binary files differnew file mode 100644 index 00000000000..cc50456370f --- /dev/null +++ b/doc/university/high-availability/aws/img/ig.png diff --git a/doc/university/high-availability/aws/img/instance_specs.png b/doc/university/high-availability/aws/img/instance_specs.png Binary files differnew file mode 100644 index 00000000000..ef31dc41dae --- /dev/null +++ b/doc/university/high-availability/aws/img/instance_specs.png diff --git a/doc/university/high-availability/aws/img/new_vpc.png b/doc/university/high-availability/aws/img/new_vpc.png Binary files differnew file mode 100644 index 00000000000..4aac6af7c7a --- /dev/null +++ b/doc/university/high-availability/aws/img/new_vpc.png diff --git a/doc/university/high-availability/aws/img/policies.png b/doc/university/high-availability/aws/img/policies.png Binary files differnew file mode 100644 index 00000000000..8c58117e4fa --- /dev/null +++ b/doc/university/high-availability/aws/img/policies.png diff --git a/doc/university/high-availability/aws/img/rds-net-opt.png b/doc/university/high-availability/aws/img/rds-net-opt.png Binary files differnew file mode 100644 index 00000000000..bc204de2474 --- /dev/null +++ b/doc/university/high-availability/aws/img/rds-net-opt.png diff --git a/doc/university/high-availability/aws/img/rds-sec-group.png b/doc/university/high-availability/aws/img/rds-sec-group.png Binary files differnew file mode 100644 index 00000000000..8864dc3e463 --- /dev/null +++ b/doc/university/high-availability/aws/img/rds-sec-group.png diff --git a/doc/university/high-availability/aws/img/redis-cluster-det.png b/doc/university/high-availability/aws/img/redis-cluster-det.png Binary files differnew file mode 100644 index 00000000000..9e9a81283c5 --- /dev/null +++ b/doc/university/high-availability/aws/img/redis-cluster-det.png diff --git a/doc/university/high-availability/aws/img/redis-net.png b/doc/university/high-availability/aws/img/redis-net.png Binary files differnew file mode 100644 index 00000000000..037bd6d6897 --- /dev/null +++ b/doc/university/high-availability/aws/img/redis-net.png diff --git a/doc/university/high-availability/aws/img/route_table.png b/doc/university/high-availability/aws/img/route_table.png Binary files differnew file mode 100644 index 00000000000..1dea322474d --- /dev/null +++ b/doc/university/high-availability/aws/img/route_table.png diff --git a/doc/university/high-availability/aws/img/subnet.png b/doc/university/high-availability/aws/img/subnet.png Binary files differnew file mode 100644 index 00000000000..dbc71201992 --- /dev/null +++ b/doc/university/high-availability/aws/img/subnet.png diff --git a/doc/university/process/README.md b/doc/university/process/README.md new file mode 100644 index 00000000000..7ff53c2cc3f --- /dev/null +++ b/doc/university/process/README.md @@ -0,0 +1,30 @@ +--- +title: University | Process +--- + +## Suggesting improvements + +If you would like to teach a class or participate or help in any way please +submit a merge request and assign it to [Job](https://gitlab.com/u/JobV). + +If you have suggestions for additional courses you would like to see, +please submit a merge request to add an upcoming class, assign to +[Chad](https://gitlab.com/u/chadmalchow) and /cc [Job](https://gitlab.com/u/JobV). + +## Adding classes + +1. All training materials of any kind should be added to [GitLab CE](https://gitlab.com/gitlab-org/gitlab-ce/) + to ensure they are available to a broad audience (don't use any other repo or + storage for training materials). +1. Don't make materials that are needlessly specific to one group of people, try + to keep the wording broad and inclusive (don't make things for only GitLab Inc. + people, only interns, only customers, etc.). +1. To allow people to contribute all content should be in git. +1. The content can go in a subdirectory under `/doc/university/`. +1. To make, view or modify the slides of the classes use [Deckset](http://www.decksetapp.com/) + or [RevealJS](http://lab.hakim.se/reveal-js/). Do not use Powerpoint or Google + Slides since this prevents everyone from contributing. +1. Please upload any video recordings to our Youtube channel. We prefer them to + be public, if needed they can be unlisted but if so they should be linked from + this page. +1. Please create a merge request and assign to [SeanPackham](https://gitlab.com/u/SeanPackham). diff --git a/doc/university/support/README.md b/doc/university/support/README.md new file mode 100644 index 00000000000..da991e56370 --- /dev/null +++ b/doc/university/support/README.md @@ -0,0 +1,188 @@ + +## Support Boot Camp + +**Goal:** Prepare new Service Engineers at GitLab + +For each stage there are learning goals and content to support the learning of the engineer. +The goal of this boot camp is to have every Service Engineer prepared to help our customers +with whatever needs they might have and to also assist our awesome community with their +questions. + +Always start with the [University Overview](../README.md) and then work +your way here for more advanced and specific training. Once you feel comfortable +with the topics of the current stage, move to the next. + +### Stage 1 + +Follow the topics on the [University Overview](../README.md), concentrate on it +during your first Stage, but also: + +- Perform the [first steps](https://about.gitlab.com/handbook/support/onboarding/#first-steps) of + the on-boarding process for new Service Engineers + +#### Goals + +Aim to have a good overview of the Product and main features, Git and the Company + +### Stage 2 + +Continue to look over remaining portions of the [University Overview](../README.md) and continue on to these topics: + +#### Set up your development machine + +Get your development machine ready to familiarize yourself with the codebase, the components, and to be prepared to reproduce issues that our users encounter + +- Install the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) + - [Setup OpenLDAP as part of this](https://gitlab.com/gitlab-org/gitlab-development-kit#openldap) + +#### Become comfortable with the Installation processes that we support + +It's important to understand how to install GitLab in the same way that our users do. Try installing different versions and upgrading and downgrading between them. Installation from source will give you a greater understanding of the components that we employ and how everything fits together. + +Sometimes we need to upgrade customers from old versions of GitLab to latest, so it's good to get some experience of doing that now. + +- [Installation Methods](https://about.gitlab.com/installation/): + - [Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/) + - [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker) + - [Source](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) +- Get yourself a Digital Ocean droplet, where you can install and maintain your own instance of GitLab + - Ask in #infrastructure about this + - Populate with some test data + - Keep this up-to-date as patch and version releases become available, just like our customers would +- Try out the following installation path + - [Install GitLab 4.2 from source](https://gitlab.com/gitlab-org/gitlab-ce/blob/d67117b5a185cfb15a1d7e749588ff981ffbf779/doc/install/installation.md) + - External MySQL database + - External NGINX + - Create some test data + - Populated Repos + - Users + - Groups + - Projects + - [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system) + - [Upgrade to 5.0 source using our Upgrade documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/4.2-to-5.0.md) + - [Upgrade to 5.1 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.0-to-5.1.md) + - [Upgrade to 6.0 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.1-to-6.0.md) + - [Upgrade to 7.14 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/6.x-or-7.x-to-7.14.md) + - [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system) + - [Perform the MySQL to PostgreSQL migration to convert your backup](http://docs.gitlab.com/ce/update/mysql_to_postgresql.html#converting-a-gitlab-backup-file-from-mysql-to-postgres) + - [Upgrade to Omnibus 7.14](http://doc.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation) + - [Restore backup using our Restore rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-a-previously-created-backup) + - [Upgrade to latest EE](https://about.gitlab.com/downloads-ee) + - (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support +- Perform a downgrade from [EE to CE](http://doc.gitlab.com/ee/downgrade_ee_to_ce/README.html) + +#### Start to learn about some of the integrations that we support + +Our integrations add great value to GitLab. User questions often relate to integrating GitLab with existing external services and the configuration involved + +- Learn about our Integrations (specially, not only): + - [LDAP](http://doc.gitlab.com/ee/integration/ldap.html) + - [JIRA](http://doc.gitlab.com/ee/project_services/jira.html) + - [Jenkins](http://doc.gitlab.com/ee/integration/jenkins.html) + - [SAML](http://doc.gitlab.com/ce/integration/saml.html) + +#### Goals + +- Aim to be comfortable with installation of the GitLab product and configuration of some of the major integrations +- Aim to have an installation available for reproducing customer reports + +### Stage 3 + +#### Understand the gathering of diagnostics for GitLab instances + +- Learn about the GitLab checks that are available + - [Environment Information and maintenance checks](http://docs.gitlab.com/ce/raketasks/maintenance.html) + - [GitLab check](http://docs.gitlab.com/ce/raketasks/check.html) + - Omnibus commands + - [Status](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#get-service-status) + - [Starting and stopping services](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#starting-and-stopping) + - [Starting a rails console](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#invoking-rake-tasks) + +#### Learn about the Support process + +Zendesk is our Support Centre and our main communication line with our Customers. We communicate with customers through several other channels too + +- Familiarize yourself with ZenDesk + - [UI Overview](https://support.zendesk.com/hc/en-us/articles/203661806-Introduction-to-the-Zendesk-agent-interface) + - [Updating Tickets](https://support.zendesk.com/hc/en-us/articles/212530318-Updating-and-solving-tickets) + - [Working w/ Tickets](https://support.zendesk.com/hc/en-us/articles/203690856-Working-with-tickets) *Read: avoiding agent collision.* +- Dive into our ZenDesk support process by reading how to [handle tickets](https://about.gitlab.com/handbook/support/onboarding/#handling-tickets) +- Start getting real world experience by handling real tickets, all the while gaining further experience with the Product. + - First, learn about our [Support Channels](https://about.gitlab.com/handbook/support/#support-channels) + - Ask other Service Engineers for help, when necessary, and to review your responses + - Start with [StackOverflow](https://about.gitlab.com/handbook/support/#stack-overflowa-namestack-overflowa) and the [GitLab forum](https://about.gitlab.com/handbook/support/#foruma-namegitlab-foruma) + - Here you will find a large variety of queries mainly from our Users who are self hosting GitLab CE + - Understand the questions that are asked and dig in to try to find a solution + - [Proceed on to the GitLab.com Support Forum](https://about.gitlab.com/handbook/support/#gitlabcom-support-trackera-namesupp-foruma) + - Here you will find queries regarding our own GitLab.com + - Helping Users here will give you an understanding of our Admin interface and other tools + - [Proceed on to the Twitter tickets in Zendesk](https://about.gitlab.com/handbook/support/#twitter) + - Here you will gain a great insight into our userbase + - Learn from any complaints and problems and feed them back to the team + - Tweets can range from help needed with GitLab installations, the API and just general queries + - [Proceed on to Regular email Support tickets](https://about.gitlab.com/handbook/support/#regular-zendesk-tickets-a-nameregulara) + - Here you will find tickets from our GitLab EE Customers and GitLab CE Users + - Tickets here are extremely varied and often very technical + - You should be prepared for these tickets, given the knowledge gained from previous tiers and your training +- Check out your colleagues' responses + - Hop on to the #support-live-feed channel in Slack and see the tickets as they come in and are updated + - Read through old tickets that your colleagues have worked on +- Start arranging to pair on calls with other Service Engineers. Aim to cover a few of each type of call + - [Learn about Cisco WebEx](https://about.gitlab.com/handbook/support/onboarding/#webexa-namewebexa) + - Training calls + - Information gathering calls + - It's good to find out how new and prospective customers are going to be using the product and how they will set up their infrastructure + - Diagnosis calls + - When email isn't enough we may need to hop on a call and do some debugging along side the customer + - These paired calls are a great learning experience + - Upgrade calls + - Emergency calls + +#### Learn about the Escalation process for tickets + +Some tickets need specific knowledge or a deep understanding of a particular component and will need to be escalated to a Senior Service Engineer or Developer + +- Read about [Escalation](https://about.gitlab.com/handbook/support/onboarding/#create-issuesa-namecreate-issuea) +- Find the macros in Zendesk for ticket escalations +- Take a look at the [GitLab.com Team page](https://about.gitlab.com/team/) to find the resident experts in their fields + +#### Learn about raising issues and fielding feature proposals + +- Understand what's in the pipeline and proposed features at GitLab: [Direction Page](https://about.gitlab.com/direction/) +- Practice searching issues and filtering using [labels](https://gitlab.com/gitlab-org/gitlab-ce/labels) to find existing feature proposals and bugs +- If raising a new issue always provide a relevant label and a link to the relevant ticket in Zendesk +- Add [customer labels](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=customer) for those issues relevant to our subscribers +- Take a look at the [existing issue templates](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker) to see what is expected +- Raise issues for bugs in a manner that would make the issue easily reproducible. A Developer or a contributor may work on your issue + +#### Goals + +- Aim to have a good understanding of the problems that customers are facing +- Aim to have gained experience in scheduling and participating in calls with customers +- Aim to have a good understanding of ticket flow through Zendesk and how to interat with our various channels + +### Stage 4 + +#### Advanced GitLab topics + +Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective + +- Set up and try [Git Annex](http://doc.gitlab.com/ee/workflow/git_annex.html) +- Set up and try [Git LFS](http://doc.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html) +- Get to know the [GitLab API](http://doc.gitlab.com/ee/api/README.html), its capabilities and shortcomings +- Learn how to [migrate from SVN to Git](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html) +- Set up [GitLab CI](http://doc.gitlab.com/ee/ci/quick_start/README.html) +- Create your first [GitLab Page](http://doc.gitlab.com/ee/pages/administration.html) +- Get to know the GitLab Codebase by reading through the source code: + - Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce) + and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce) +- Ask as many questions as you can think of on the `#support` chat channel + +#### Get initiated for on-call duty + +- Read over the [public run-books to understand common tasks](https://gitlab.com/gitlab-com/runbooks) +- Create an issue on the internal Organization tracker to schedule time with the DevOps / Production team, so that you learn how to handle GitLab.com going down. Once you are trained for this, you are ready to be added to the on-call rotation. + +#### Goals + +- Aim to become a fully-fledged Service Engineer! diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md new file mode 100644 index 00000000000..03c62a81b10 --- /dev/null +++ b/doc/university/training/end-user/README.md @@ -0,0 +1,420 @@ + +# Training + +This training material is the markdown used to generate training slides +which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-training-slides/#/) +through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides) +project. + +--- + +## Git Intro + +--- + +### What is a Version Control System (VCS) + +- Records changes to a file +- Maintains history of changes +- Disaster Recovery +- Types of VCS: Local, Centralized and Distributed + +--- + +### Short Story of Git + +- 1991-2002: The Linux kernel was being maintaned by sharing archived files + and patches. +- 2002: The Linux kernel project began using a DVCS called BitKeeper +- 2005: BitKeeper revoked the free-of-charge status and Git was created + +--- + +### What is Git + +- Distributed Version Control System +- Great branching model that adapts well to most workflows +- Fast and reliable +- Keeps a complete history +- Disaster recovery friendly +- Open Source + +--- + +### Getting Help + +- Use the tools at your disposal when you get stuck. + - Use `git help <command>` command + - Use Google (i.e. StackOverflow, Google groups) + - Read documentation at https://git-scm.com + +--- + +## Git Setup +Workshop Time! + +--- + +### Setup + +- Windows: Install 'Git for Windows' + - https://git-for-windows.github.io +- Mac: Type `git` in the Terminal application. + - If it's not installed, it will prompt you to install it. +- Linux + - Debian: `sudo apt-get install git-all` + - Red Hat `sudo yum install git-all` + +--- + +### Configure + +- One-time configuration of the Git client: + +```bash +git config --global user.name "Your Name" +git config --global user.email you@example.com +``` + +- If you don't use the global flag you can setup a different author for + each project +- Check settings with: + +```bash +git config --global --list +``` +- You might want or be required to use an SSH key. + - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html) + +--- + +### Workspace + +- Choose a directory on you machine easy to access +- Create a workspace or development directory +- This is where we'll be working and adding content + +--- + +```bash +mkdir ~/development +cd ~/development + +-or- + +mkdir ~/workspace +cd ~/workspace +``` + +--- + +## Git Basics + +--- + +### Git Workflow + +- Untracked files + - New files that Git has not been told to track previously. +- Working area (Workspace) + - Files that have been modified but are not committed. +- Staging area (Index) + - Modified files that have been marked to go in the next commit. +- Upstream + - Hosted repository on a shared server + +--- + +### GitLab + +- GitLab is an application to code, test and deploy. +- Provides repository management with access controls, code reviews, + issue tracking, Merge Requests, and other features. +- The hosted version of GitLab is gitlab.com + +--- + +### New Project + +- Sign in into your gitlab.com account +- Create a project +- Choose to import from 'Any Repo by URL' and use https://gitlab.com/gitlab-org/training-examples.git +- On your machine clone the `training-examples` project + +--- + +### Git and GitLab basics + +1. Edit `edit_this_file.rb` in `training-examples` +2. See it listed as a changed file (working area) +3. View the differences +4. Stage the file +5. Commit +6. Push the commit to the remote +7. View the git log + +--- + +```shell +# Edit `edit_this_file.rb` +git status +git diff +git add <file> +git commit -m 'My change' +git push origin master +git log +``` + +--- + +### Feature Branching + +1. Create a new feature branch called `squash_some_bugs` +2. Edit `bugs.rb` and remove all the bugs. +3. Commit +4. Push + +--- + +```shell +git checkout -b squash_some_bugs +# Edit `bugs.rb` +git status +git add bugs.rb +git commit -m 'Fix some buggy code' +git push origin squash_some_bugs +``` + +--- + +## Merge Request + +--- + +### Merge requests + +- When you want feedback create a merge request +- Target is the ‘default’ branch (usually master) +- Assign or mention the person you would like to review +- Add `WIP` to the title if it's a work in progress +- When accepting, always delete the branch +- Anyone can comment, not just the assignee +- Push corrections to the same branch + + +--- + +### Merge request example + +- Create your first merge request + - Use the blue button in the activity feed + - View the diff (changes) and leave a comment + - Push a new commit to the same branch + - Review the changes again and notice the update + +--- + +### Feedback and Collaboration + +- Merge requests are a time for feedback and collaboration +- Giving feedback is hard +- Be as kind as possible +- Receiving feedback is hard +- Be as receptive as possible +- Feedback is about the best code, not the person. You are not your code +- Feedback and Collaboration + +--- + +### Feedback and Collaboration + +- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:[Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review) +- See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) + +--- + +## Merge Conflicts + +--- + +### Merge Conflicts +* Happen often +* Learning to fix conflicts is hard +* Practice makes perfect +* Force push after fixing conflicts. Be careful! + +--- + +### Example Plan +1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'. +2. Commit and push +3. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'. +4. Commit and push to master +5. Create a merge request and watch it fail +6. Rebase our new branch with master +7. Fix conflicts on the conflicts.rb file. +8. Stage the file and continue rebasing +9. Force push the changes +10. Finally continue with the Merge Request + +--- + +### Example 1/2 + + git checkout -b conflicts_branch + + # vi conflicts.rb + # Add 'Line4' and 'Line5' + + git commit -am "add line4 and line5" + git push origin conflicts_branch + + git checkout master + + # vi conflicts.rb + # Add 'Line6' and 'Line7' + git commit -am "add line6 and line7" + git push origin master + +--- + +### Example 2/2 + +Create a merge request on the GitLab web UI. You'll see a conflict warning. + + git checkout conflicts_branch + git fetch + git rebase master + + # Fix conflicts by editing the files. + + git add conflicts.rb + # No need to commit this file + + git rebase --continue + + # Remember that we have rewritten our commit history so we + # need to force push so that our remote branch is restructured + git push origin conflicts_branch -f + +--- + +### Notes + +* When to use `git merge` and when to use `git rebase` +* Rebase when updating your branch with master +* Merge when bringing changes from feature to master +* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/ + +--- + +## Revert and Unstage + +--- + +### Unstage + +To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch: + + git reset HEAD <file> + +This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use: + + git checkout -- <file> + +To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag: + + git rm '*.txt' + git rm -r <dirname> + +If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our .gitignore file then use `--cache`: + + git rm <filename> --cache + +--- + +### Undo Commits + +Undo last commit putting everything back into the staging area: + + git reset --soft HEAD^ + +Add files and change message with: + + git commit --amend -m "New Message" + +Undo last and remove changes + + git reset --hard HEAD^ + +Same as last one but for two commits back: + + git reset --hard HEAD^^ + +Don't reset after pushing + +--- + +### Reset Workflow + +1. Edit file again 'edit_this_file.rb' +2. Check status +3. Add and commit with wrong message +4. Check log +5. Amend commit +6. Check log +7. Soft reset +8. Check log +9. Pull for updates +10. Push changes + +---- + + # Change file edit_this_file.rb + git status + git commit -am "kjkfjkg" + git log + git commit --amend -m "New comment added" + git log + git reset --soft HEAD^ + git log + git pull origin master + git push origin master + +--- + +### Note + +git revert vs git reset +Reset removes the commit while revert removes the changes but leaves the commit +Revert is safer considering we can revert a revert + + + # Changed file + git commit -am "bug introduced" + git revert HEAD + # New commit created reverting changes + # Now we want to re apply the reverted commit + git log # take hash from the revert commit + git revert <rev commit hash> + # reverted commit is back (new commit created again) + +--- + +## Questions + +--- + +## Instructor Notes + +--- + +### Version Control + - Local VCS was used with a filesystem or a simple db. + - Centralized VCS such as Subversion includes collaboration but + still is prone to data loss as the main server is the single point of + failure. + - Distributed VCS enables the team to have a complete copy of the project + and work with little dependency to the main server. In case of a main + server failing the project can be recovered by any of the latest copies + from the team diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index b058f8e2a03..b24d338e3e0 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ### 3. Update Ruby -If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible. +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`. diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index 076696f565b..ee9fb1a2a68 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ### 3. Update Ruby -If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible. +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`. diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md new file mode 100644 index 00000000000..411e4837e20 --- /dev/null +++ b/doc/update/8.12-to-8.13.md @@ -0,0 +1,201 @@ +# From 8.12 to 8.13 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. 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.1.tar.gz +echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz +cd ruby-2.3.1 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-13-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-13-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 v3.6.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. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.8.2 +sudo -u git -H make +``` + +### 7. 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 +``` + +### 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 +git diff origin/8-12-stable:config/gitlab.yml.example origin/8-13-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 +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 +# For HTTPS configurations +git diff origin/8-12-stable:lib/support/nginx/gitlab-ssl origin/8-13-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-12-stable:lib/support/nginx/gitlab origin/8-13-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-13-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-13-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 9. Start application + + 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 + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.12) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.11 to 8.12](8.11-to-8.12.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/user/markdown.md b/doc/user/markdown.md index c7fda8a497f..56e5b802a52 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -27,6 +27,7 @@ * [Horizontal Rule](#horizontal-rule) * [Line Breaks](#line-breaks) * [Tables](#tables) +* [Footnotes](#footnotes) **[Wiki-Specific Markdown](#wiki-specific-markdown)** @@ -699,6 +700,15 @@ By including colons in the header row, you can align the text within that column | Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | +## Footnotes + +You can add footnotes to your text as follows.[^1] +[^1]: This is my awesome footnote. + +``` +You can add footnotes to your text as follows.[^1] +[^1]: This is my awesome footnote. +``` ## Wiki-specific Markdown diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 12d5b8f8744..c0dc80325b6 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -23,6 +23,7 @@ The following table depicts the various user permission levels in a project. | See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ | | Pull project code | | ✓ | ✓ | ✓ | ✓ | | Download project | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 445c0ee8333..65ed9fae4ec 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -7,7 +7,8 @@ > that of the exporter. > - For existing installations, the project import option has to be enabled in > application settings (`/admin/application_settings`) under 'Import sources'. -> You will have to be an administrator to enable and use the import functionality. +> Ask your administrator if you don't see the **GitLab export** button when +> creating a new project. > - You can find some useful raketasks if you are an administrator in the > [import_export](../../../administration/raketasks/project_import_export.md) > raketask. diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 1792a0c501d..5f6a6c6503e 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -27,4 +27,5 @@ do. | `/subscribe` | Subscribe | | `/unsubscribe` | Unsubscribe | | <code>/due <in 2 days | this Friday | December 31st></code> | Set due date | -| `/remove_due_date` | Remove due date | +| `/remove_due_date` | Remove due date | +| `/wip` | Toggle the Work In Progress status | diff --git a/features/profile/ssh_keys.feature b/features/profile/ssh_keys.feature deleted file mode 100644 index b0d5b748916..00000000000 --- a/features/profile/ssh_keys.feature +++ /dev/null @@ -1,20 +0,0 @@ -@profile -Feature: Profile SSH Keys - Background: - Given I sign in as a user - And I have ssh key "ssh-rsa Work" - And I visit profile keys page - - Scenario: I should see ssh keys - Then I should see my ssh keys - - Scenario: Add new ssh key - Given I should see new ssh key form - And I submit new ssh key "Laptop" - Then I should see new ssh key "Laptop" - - Scenario: Remove ssh key - Given I click link "Work" - And I click link "Remove" - Then I visit profile keys page - And I should not see "Work" ssh key diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 358e622b736..80670063ea0 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -37,6 +37,7 @@ Feature: Project Issues And I submit new issue "500 error on profile" Then I should see issue "500 error on profile" + @javascript Scenario: I submit new unassigned issue with labels Given project "Shop" has labels: "bug", "feature", "enhancement" And I click link "New Issue" diff --git a/features/project/snippets.feature b/features/project/snippets.feature index 270557cbde7..3c51ea56585 100644 --- a/features/project/snippets.feature +++ b/features/project/snippets.feature @@ -12,7 +12,7 @@ Feature: Project Snippets And I should not see "Snippet two" in snippets Scenario: I create new project snippet - Given I click link "New Snippet" + Given I click link "New snippet" And I submit new snippet "Snippet three" Then I should see snippet "Snippet three" diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 2f0941e4113..cb36d6ae1a9 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -20,6 +20,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps expect(page).to have_link('GitLab.com') expect(page).to have_link('Google Code') expect(page).to have_link('Repo by URL') + expect(page).to have_link('GitLab export') end step 'I click on "Import project from GitHub"' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 4ee6784a086..05ab2a7dc73 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -13,6 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps fill_in 'user_website_url', with: 'testurl' fill_in 'user_location', with: 'Ukraine' fill_in 'user_bio', with: 'I <3 GitLab' + fill_in 'user_organization', with: 'GitLab' click_button 'Update profile settings' @user.reload end @@ -23,6 +24,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps expect(@user.twitter).to eq 'testtwitter' expect(@user.website_url).to eq 'testurl' expect(@user.bio).to eq 'I <3 GitLab' + expect(@user.organization).to eq 'GitLab' expect(find('#user_location').value).to eq 'Ukraine' end diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb deleted file mode 100644 index a400488a532..00000000000 --- a/features/steps/profile/ssh_keys.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps - include SharedAuthentication - - step 'I should see my ssh keys' do - @user.keys.each do |key| - expect(page).to have_content(key.title) - end - end - - step 'I should see new ssh key form' do - expect(page).to have_content("Add an SSH key") - end - - step 'I submit new ssh key "Laptop"' do - fill_in "key_title", with: "Laptop" - fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" - click_button "Add key" - end - - step 'I should see new ssh key "Laptop"' do - key = Key.find_by(title: "Laptop") - expect(page).to have_content(key.title) - expect(page).to have_content(key.key) - expect(current_path).to eq profile_key_path(key) - end - - step 'I click link "Work"' do - click_link "Work" - end - - step 'I click link "Remove"' do - click_link "Remove" - end - - step 'I visit profile keys page' do - visit profile_keys_path - end - - step 'I should not see "Work" ssh key' do - expect(page).not_to have_content "Work" - end - - step 'I have ssh key "ssh-rsa Work"' do - create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work") - end -end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index dacab6c7977..6c14d835004 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -138,19 +138,19 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I click "Assign to" dropdown"' do - first('.ajax-users-select').click + click_button 'Assignee' end step 'I should see the target project ID in the input selector' do - expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]") + expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}" end step 'I should see the users from the target project ID' do - expect(page).to have_selector('.user-result', visible: true, count: 3) - users = page.all('.user-name') - expect(users[0].text).to eq 'Unassigned' - expect(users[1].text).to eq current_user.name - expect(users[2].text).to eq @project.users.first.name + page.within '.dropdown-menu-user' do + expect(page).to have_content 'Unassigned' + expect(page).to have_content current_user.name + expect(page).to have_content @project.users.first.name + end end # Verify a link is generated against the correct project diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index ed7241679ee..b50f5238e80 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -84,7 +84,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I submit new issue "500 error on profile" with label \'bug\'' do fill_in "issue_title", with: "500 error on profile" - select 'bug', from: "Labels" + click_button "Label" + click_link "bug" click_button "Submit issue" end diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index beb8ecfc799..5e7d539add6 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -21,8 +21,8 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps author: project.users.first) end - step 'I click link "New Snippet"' do - click_link "New Snippet" + step 'I click link "New snippet"' do + click_link "New snippet" end step 'I click link "Snippet one"' do diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 29a97ccbd75..d3db7740830 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -16,9 +16,9 @@ module API # GET /projects/:id/access_requests get ":id/access_requests" do source = find_source(source_type, params[:id]) - authorize_admin_source!(source_type, source) - access_requesters = paginate(source.requesters.includes(:user)) + access_requesters = AccessRequestsFinder.new(source).execute!(current_user) + access_requesters = paginate(access_requesters.includes(:user)) present access_requesters.map(&:user), with: Entities::AccessRequester, source: source end @@ -55,13 +55,8 @@ module API put ':id/access_requests/:user_id/approve' do required_attributes! [:user_id] source = find_source(source_type, params[:id]) - authorize_admin_source!(source_type, source) - member = source.requesters.find_by!(user_id: params[:user_id]) - if params[:access_level] - member.update(access_level: params[:access_level]) - end - member.accept_request + member = ::Members::ApproveAccessRequestService.new(source, current_user, params).execute status :created present member.user, with: Entities::Member, member: member @@ -80,9 +75,8 @@ module API required_attributes! [:user_id] source = find_source(source_type, params[:id]) - access_requester = source.requesters.find_by!(user_id: params[:user_id]) - - ::Members::DestroyService.new(access_requester, current_user).execute + ::Members::DestroyService.new(source, current_user, params). + execute(:requesters) end end end diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 2461a783ea8..e9ccba3b465 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -8,16 +8,19 @@ module API awardable_string = awardable_type.pluralize awardable_id_string = "#{awardable_type}_id" + params do + requires :id, type: String, desc: 'The ID of a project' + requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" + end + [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" ].each do |endpoint| - # Get a list of project +awardable+ award emoji - # - # Parameters: - # id (required) - The ID of a project - # awardable_id (required) - The ID of an issue or MR - # Example Request: - # GET /projects/:id/issues/:awardable_id/award_emoji + + desc 'Get a list of project +awardable+ award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end get endpoint do if can_read_awardable? awards = paginate(awardable.award_emoji) @@ -27,14 +30,13 @@ module API end end - # Get a specific award emoji - # - # Parameters: - # id (required) - The ID of a project - # awardable_id (required) - The ID of an issue or MR - # award_id (required) - The ID of the award - # Example Request: - # GET /projects/:id/issues/:awardable_id/award_emoji/:award_id + desc 'Get a specific award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :award_id, type: Integer, desc: 'The ID of the award' + end get "#{endpoint}/:award_id" do if can_read_awardable? present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji @@ -43,17 +45,14 @@ module API end end - # Award a new Emoji - # - # Parameters: - # id (required) - The ID of a project - # awardable_id (required) - The ID of an issue or mr - # name (required) - The name of a award_emoji (without colons) - # Example Request: - # POST /projects/:id/issues/:awardable_id/award_emoji + desc 'Award a new Emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :name, type: String, desc: 'The name of a award_emoji (without colons)' + end post endpoint do - required_attributes! [:name] - not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? award = awardable.create_award_emoji(params[:name], current_user) @@ -65,14 +64,13 @@ module API end end - # Delete a +awardables+ award emoji - # - # Parameters: - # id (required) - The ID of a project - # awardable_id (required) - The ID of an issue or MR - # award_emoji_id (required) - The ID of an award emoji - # Example Request: - # DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id + desc 'Delete a +awardables+ award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :award_id, type: Integer, desc: 'The ID of an award emoji' + end delete "#{endpoint}/:award_id" do award = awardable.award_emoji.find(params[:award_id]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 92a6f29adb0..04437322ec1 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -15,7 +15,7 @@ module API class User < UserBasic expose :created_at expose :is_admin?, as: :is_admin - expose :bio, :location, :skype, :linkedin, :twitter, :website_url + expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization end class Identity < Grape::Entity @@ -343,7 +343,7 @@ module API end class ProjectGroupLink < Grape::Entity - expose :id, :project_id, :group_id, :group_access + expose :id, :project_id, :group_id, :group_access, :expires_at end class Todo < Grape::Entity @@ -494,6 +494,8 @@ module API expose :after_sign_out_path expose :container_registry_token_expire_delay expose :repository_storage + expose :koding_enabled + expose :koding_url end class Release < Grape::Entity @@ -545,6 +547,10 @@ module API expose :filename, :size end + class PipelineBasic < Grape::Entity + expose :id, :sha, :ref, :status + end + class Build < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at @@ -552,6 +558,7 @@ module API expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } expose :commit, with: RepoCommit expose :runner, with: Runner + expose :pipeline, with: PipelineBasic end class Trigger < Grape::Entity @@ -562,8 +569,8 @@ module API expose :key, :value end - class Pipeline < Grape::Entity - expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors + class Pipeline < PipelineBasic + expose :before_sha, :tag, :yaml_errors expose :user, with: Entities::UserBasic expose :created_at, :updated_at, :started_at, :finished_at, :committed_at diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 953fa474e88..bfb89475025 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -6,6 +6,8 @@ module API resource :groups do # Get a groups list # + # Parameters: + # skip_groups (optional) - Array of group ids to exclude from list # Example Request: # GET /groups get do @@ -16,6 +18,7 @@ module API end @groups = @groups.search(params[:search]) if params[:search].present? + @groups = @groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? @groups = paginate @groups present @groups, with: Entities::Group end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 090d04544da..9a5d1ece070 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -90,7 +90,7 @@ module API { username: token_handler.actor_name, - lfs_token: token_handler.generate, + lfs_token: token_handler.token, repository_http_path: project.http_url_to_repo } end diff --git a/lib/api/keys.rb b/lib/api/keys.rb index 2b723b79504..767f27ef334 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -4,10 +4,9 @@ module API before { authenticate! } resource :keys do - # Get single ssh key by id. Only available to admin users. - # - # Example Request: - # GET /keys/:id + desc 'Get single ssh key by id. Only available to admin users' do + success Entities::SSHKeyWithUser + end get ":id" do authenticated_as_admin! diff --git a/lib/api/members.rb b/lib/api/members.rb index 37f0a6512f4..34df55fe192 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -59,13 +59,6 @@ module API authorize_admin_source!(source_type, source) required_attributes! [:user_id, :access_level] - access_requester = source.requesters.find_by(user_id: params[:user_id]) - if access_requester - # We pass current_user = access_requester so that the requester doesn't - # receive a "access denied" email - ::Members::DestroyService.new(access_requester, access_requester.user).execute - end - member = source.members.find_by(user_id: params[:user_id]) # This is to ensure back-compatibility but 409 behavior should be used @@ -73,18 +66,12 @@ module API conflict!('Member already exists') if source_type == 'group' && member unless member - source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) - member = source.members.find_by(user_id: params[:user_id]) + member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) end - if member + if member.persisted? && member.valid? present member.user, with: Entities::Member, member: member else - # Since `source.add_user` doesn't return a member object, we have to - # build a new one and populate its errors in order to render them. - member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at])) - member.valid? # populate the errors - # This is to ensure back-compatibility but 400 behavior should be used # for all validation errors in 9.0! render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) @@ -147,7 +134,7 @@ module API if member.nil? { message: "Access revoked", id: params[:user_id].to_i } else - ::Members::DestroyService.new(member, current_user).execute + ::Members::DestroyService.new(source, current_user, params).execute present member.user, with: Entities::Member, member: member end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 7a0cb7c99f3..9b73f6826cf 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -108,8 +108,7 @@ module API finder_params = { project_id: user_project.id, - milestone_title: @milestone.title, - state: 'all' + milestone_title: @milestone.title } issues = IssuesFinder.new(current_user, finder_params).execute diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 50d3729449e..fe981d7b9fa 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -4,20 +4,18 @@ module API before { authenticate! } resource :namespaces do - # Get a namespaces list - # - # Example Request: - # GET /namespaces + desc 'Get a namespaces list' do + success Entities::Namespace + end + params do + optional :search, type: String, desc: "Search query for namespaces" + end get do - @namespaces = if current_user.admin - Namespace.all - else - current_user.namespaces - end - @namespaces = @namespaces.search(params[:search]) if params[:search].present? - @namespaces = paginate @namespaces + namespaces = current_user.admin ? Namespace.all : current_user.namespaces + + namespaces = namespaces.search(params[:search]) if params[:search].present? - present @namespaces, with: Entities::Namespace + present paginate(namespaces), with: Entities::Namespace end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6d99617b56f..680055c95eb 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -393,23 +393,24 @@ module API # Share project with group # # Parameters: - # id (required) - The ID of a project - # group_id (required) - The ID of a group + # id (required) - The ID of a project + # group_id (required) - The ID of a group # group_access (required) - Level of permissions for sharing + # expires_at (optional) - Share expiration date # # Example Request: # POST /projects/:id/share post ":id/share" do authorize! :admin_project, user_project required_attributes! [:group_id, :group_access] + attrs = attributes_for_keys [:group_id, :group_access, :expires_at] unless user_project.allowed_to_share_with_group? return render_api_error!("The project sharing with group is disabled", 400) end - link = user_project.project_group_links.new - link.group_id = params[:group_id] - link.group_access = params[:group_access] + link = user_project.project_group_links.new(attrs) + if link.save present link, with: Entities::ProjectGroupLink else diff --git a/lib/api/users.rb b/lib/api/users.rb index c440305ff0f..18c4cad09ae 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -60,6 +60,7 @@ module API # linkedin - Linkedin # twitter - Twitter account # website_url - Website url + # organization - Organization # projects_limit - Number of projects user can create # extern_uid - External authentication provider UID # provider - External provider @@ -74,7 +75,7 @@ module API post do authenticated_as_admin! required_attributes! [:email, :password, :name, :username] - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization] admin = attrs.delete(:admin) confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i) user = User.build_user(attrs) @@ -111,6 +112,7 @@ module API # linkedin - Linkedin # twitter - Twitter account # website_url - Website url + # organization - Organization # projects_limit - Limit projects each user can create # bio - Bio # location - Location of the user @@ -122,7 +124,7 @@ module API put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization] user = User.find(params[:id]) not_found!('User') unless user diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 16cd774c81a..affe34394c2 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -64,7 +64,7 @@ module Banzai end end - def project_from_ref_cache(ref) + def project_from_ref_cached(ref) if RequestStore.active? cache = project_refs_cache @@ -146,7 +146,7 @@ module Banzai # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_text: nil) references_in(text, pattern) do |match, id, project_ref, matches| - project = project_from_ref_cache(project_ref) + project = project_from_ref_cached(project_ref) if project && object = find_object_cached(project, id) title = object_link_title(object) @@ -243,11 +243,27 @@ module Banzai end end - # Returns the projects for the given paths. - def find_projects_for_paths(paths) + def projects_relation_for_paths(paths) Project.where_paths_in(paths).includes(:namespace) end + # Returns projects for the given paths. + def find_projects_for_paths(paths) + if RequestStore.active? + to_query = paths - project_refs_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 } + end + end + + project_refs_cache.slice(*paths).values + else + projects_relation_for_paths(paths) + end + end + def current_project_path @current_project_path ||= project.path_with_namespace end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 4042e9a4c25..54c5f9a71a4 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -66,7 +66,7 @@ module Banzai end end - def find_projects_for_paths(paths) + def projects_relation_for_paths(paths) super(paths).includes(:gitlab_issue_tracker_service) end end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 2470362e019..af1e575fc89 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -25,7 +25,7 @@ module Banzai return if customized?(whitelist[:transformers]) # Allow code highlighting - whitelist[:attributes]['pre'] = %w(class) + whitelist[:attributes]['pre'] = %w(class v-pre) whitelist[:attributes]['span'] = %w(class) # Allow table alignment diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index fcdb496aed2..026b81ac175 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -30,7 +30,7 @@ module Banzai # users can still access an issue/comment/etc. end - highlighted = %(<pre class="#{css_classes}"><code>#{code}</code></pre>) + highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>) # Extracted to a method to measure it replace_parent_pre_element(node, highlighted) diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb index 66608c9859c..9fa5f589f3e 100644 --- a/lib/banzai/filter/task_list_filter.rb +++ b/lib/banzai/filter/task_list_filter.rb @@ -2,27 +2,7 @@ require 'task_list/filter' module Banzai module Filter - # Work around a bug in the default TaskList::Filter that adds a `task-list` - # class to every list element, regardless of whether or not it contains a - # task list. - # - # This is a (hopefully) temporary fix, pending a new release of the - # task_list gem. - # - # See https://github.com/github/task_list/pull/60 class TaskListFilter < TaskList::Filter - def add_css_class_with_fix(node, *new_class_names) - if new_class_names.include?('task-list') - # Don't add class to all lists - return - elsif new_class_names.include?('task-list-item') - add_css_class_without_fix(node.parent, 'task-list') - end - - add_css_class_without_fix(node, *new_class_names) - end - - alias_method_chain :add_css_class, :fix end end end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index e1ca7f4d24b..c6302b586d3 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -106,13 +106,17 @@ module Banzai project = context[:project] author = context[:author] - url = urls.namespace_project_url(project.namespace, project, - only_path: context[:only_path]) + if author && !project.team.member?(author) + link_text + else + url = urls.namespace_project_url(project.namespace, project, + only_path: context[:only_path]) - data = data_attribute(project: project.id, author: author.try(:id)) - text = link_text || User.reference_prefix + 'all' + data = data_attribute(project: project.id, author: author.try(:id)) + text = link_text || User.reference_prefix + 'all' - link_tag(url, data, text, 'All Project and Group Members') + link_tag(url, data, text, 'All Project and Group Members') + end end def link_to_namespace(namespace, link_text: nil) diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index e8e03e4a98f..f5d110e987b 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -79,7 +79,11 @@ module Banzai def referenced_by(nodes) ids = unique_attribute_values(nodes, self.class.data_attribute) - references_relation.where(id: ids) + if ids.empty? + references_relation.none + else + references_relation.where(id: ids) + end end # Returns the ActiveRecord::Relation to use for querying references in the diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 59f85416ee5..ed87a2603e8 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -12,10 +12,9 @@ module Ci # POST /builds/register post "register" do authenticate_runner! - update_runner_last_contact(save: false) - update_runner_info required_attributes! [:token] not_found! unless current_runner.active? + update_runner_info build = Ci::RegisterBuildService.new.execute(current_runner) @@ -41,10 +40,11 @@ module Ci # PUT /builds/:id put ":id" do authenticate_runner! - update_runner_last_contact build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id]) forbidden!('Build has been erased!') if build.erased? + update_runner_info + build.update_attributes(trace: params[:trace]) if params[:trace] Gitlab::Metrics.add_event(:update_build, diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 23353c62885..e608f5f6cad 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -3,7 +3,7 @@ module Ci module Helpers BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN" BUILD_TOKEN_PARAM = :token - UPDATE_RUNNER_EVERY = 40 * 60 + UPDATE_RUNNER_EVERY = 10 * 60 def authenticate_runners! forbidden! unless runner_registration_token_valid? @@ -30,14 +30,22 @@ module Ci token && (build.valid_token?(token) || build.project.valid_runners_token?(token)) end - def update_runner_last_contact(save: true) - # Use a random threshold to prevent beating DB updates - # it generates a distribution between: [40m, 80m] + def update_runner_info + return unless update_runner? + + current_runner.contacted_at = Time.now + current_runner.assign_attributes(get_runner_version_from_params) + current_runner.save if current_runner.changed? + end + + def update_runner? + # Use a random threshold to prevent beating DB updates. + # It generates a distribution between [40m, 80m]. + # contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) - if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age - current_runner.contacted_at = Time.now - current_runner.save if current_runner.changed? && save - end + + current_runner.contacted_at.nil? || + (Time.now - current_runner.contacted_at) >= contacted_at_max_age end def build_not_found! @@ -57,11 +65,6 @@ module Ci attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"]) end - def update_runner_info - current_runner.assign_attributes(get_runner_version_from_params) - current_runner.save if current_runner.changed? - end - def max_artifacts_size current_application_settings.max_artifacts_size.megabytes.to_i end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 0369e80312a..2fd1fced65c 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -4,7 +4,7 @@ module Ci include Gitlab::Ci::Config::Node::LegacyValidationHelpers - attr_reader :path, :cache, :stages + attr_reader :path, :cache, :stages, :jobs def initialize(config, path = nil) @ci_config = Gitlab::Ci::Config.new(config) diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb deleted file mode 100644 index 2a87c91db5e..00000000000 --- a/lib/ci/version_info.rb +++ /dev/null @@ -1,52 +0,0 @@ -class VersionInfo - include Comparable - - attr_reader :major, :minor, :patch - - def self.parse(str) - if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/) - VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i) - else - VersionInfo.new - end - end - - def initialize(major = 0, minor = 0, patch = 0) - @major = major - @minor = minor - @patch = patch - end - - def <=>(other) - return unless other.is_a? VersionInfo - return unless valid? && other.valid? - - if other.major < @major - 1 - elsif @major < other.major - -1 - elsif other.minor < @minor - 1 - elsif @minor < other.minor - -1 - elsif other.patch < @patch - 1 - elsif @patch < other.patch - -1 - else - 0 - end - end - - def to_s - if valid? - "%d.%d.%d" % [@major, @minor, @patch] - else - "Unknown" - end - end - - def valid? - @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 - end -end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index a533bac2692..9b484a2ecfd 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -53,6 +53,10 @@ module Gitlab } end + def sym_options_with_owner + sym_options.merge(owner: OWNER) + end + def protection_options { "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 7c0f2115d43..aca5d0020cf 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -124,7 +124,7 @@ module Gitlab read_authentication_abilities end - Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.value, password) + Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password) end def build_access_token_check(login, password) diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 084e514492c..e33ac61f5ae 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -52,7 +52,7 @@ module Gitlab def method_missing(method, *args, &block) if api.respond_to?(method) - request { api.send(method, *args, &block) } + request(method, *args, &block) else super(method, *args, &block) end @@ -99,20 +99,19 @@ module Gitlab rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME end - def request + def request(method, *args, &block) sleep rate_limit_sleep_time if rate_limit_exceed? - data = yield + data = api.send(method, *args, &block) + yield data last_response = api.last_response while last_response.rels[:next] sleep rate_limit_sleep_time if rate_limit_exceed? last_response = last_response.rels[:next].get - data.concat(last_response.data) if last_response.data.is_a?(Array) + yield last_response.data if last_response.data.is_a?(Array) end - - data end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index d35ee2a1c65..4b70f33a851 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -10,6 +10,7 @@ module Gitlab @repo = project.import_source @repo_url = project.import_url @errors = [] + @labels = {} if credentials @client = Client.new(credentials[:user]) @@ -23,6 +24,7 @@ module Gitlab import_milestones import_issues import_pull_requests + import_comments import_wiki import_releases handle_errors @@ -46,66 +48,68 @@ module Gitlab end def import_labels - labels = client.labels(repo, per_page: 100) - - labels.each do |raw| - begin - LabelFormatter.new(project, raw).create! - rescue => e - errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + client.labels(repo, per_page: 100) do |labels| + labels.each do |raw| + begin + label = LabelFormatter.new(project, raw).create! + @labels[label.title] = label.id + rescue => e + errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end end end end def import_milestones - milestones = client.milestones(repo, state: :all, per_page: 100) - - milestones.each do |raw| - begin - MilestoneFormatter.new(project, raw).create! - rescue => e - errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + client.milestones(repo, state: :all, per_page: 100) do |milestones| + milestones.each do |raw| + begin + MilestoneFormatter.new(project, raw).create! + rescue => e + errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end end end end def import_issues - issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) - - issues.each do |raw| - gh_issue = IssueFormatter.new(project, raw) - - if gh_issue.valid? - begin - issue = gh_issue.create! - apply_labels(issue) - import_comments(issue) if gh_issue.has_comments? - rescue => e - errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| + issues.each do |raw| + gh_issue = IssueFormatter.new(project, raw) + + if gh_issue.valid? + begin + issue = gh_issue.create! + apply_labels(issue, raw) + rescue => e + errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end end end end end def import_pull_requests - pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) - pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) - - pull_requests.each do |pull_request| - begin - restore_source_branch(pull_request) unless pull_request.source_branch_exists? - restore_target_branch(pull_request) unless pull_request.target_branch_exists? - - merge_request = pull_request.create! - apply_labels(merge_request) - import_comments(merge_request) - import_comments_on_diff(merge_request) - rescue => e - errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message } - ensure - clean_up_restored_branches(pull_request) + client.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? + + begin + restore_source_branch(pull_request) unless pull_request.source_branch_exists? + restore_target_branch(pull_request) unless pull_request.target_branch_exists? + + merge_request = pull_request.create! + apply_labels(merge_request, raw) + rescue => e + errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message } + ensure + clean_up_restored_branches(pull_request) + end end end + + project.repository.after_remove_branch end def restore_source_branch(pull_request) @@ -125,37 +129,38 @@ module Gitlab def clean_up_restored_branches(pull_request) remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists? remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists? - - project.repository.after_remove_branch end - def apply_labels(issuable) - issue = client.issue(repo, issuable.iid) - - if issue.labels.count > 0 - label_ids = issue.labels - .map { |attrs| project.labels.find_by(title: attrs.name).try(:id) } + def apply_labels(issuable, raw_issuable) + if raw_issuable.labels.count > 0 + label_ids = raw_issuable.labels + .map { |attrs| @labels[attrs.name] } .compact issuable.update_attribute(:label_ids, label_ids) end end - def import_comments(issuable) - comments = client.issue_comments(repo, issuable.iid, per_page: 100) - create_comments(issuable, comments) - end + def import_comments + client.issues_comments(repo, per_page: 100) do |comments| + create_comments(comments, :issue) + end - def import_comments_on_diff(merge_request) - comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100) - create_comments(merge_request, comments) + client.pull_requests_comments(repo, per_page: 100) do |comments| + create_comments(comments, :pull_request) + end end - def create_comments(issuable, comments) + def create_comments(comments, issuable_type) ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = CommentFormatter.new(project, raw) + comment = CommentFormatter.new(project, raw) + issuable_class = issuable_type == :issue ? Issue : MergeRequest + iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly + issuable = issuable_class.find_by_iid(iid) + next unless issuable + issuable.notes.create!(comment.attributes) rescue => e errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } @@ -165,10 +170,9 @@ module Gitlab end def import_wiki - unless project.wiki_enabled? + unless project.wiki.repository_exists? wiki = WikiFormatter.new(project) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) - project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) end rescue Gitlab::Shell::Error => e # GitHub error message when the wiki repo has not been created, @@ -180,13 +184,14 @@ module Gitlab end def import_releases - releases = client.releases(repo, per_page: 100) - releases.each do |raw| - begin - 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 } + client.releases(repo, per_page: 100) do |releases| + releases.each do |raw| + begin + 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 } + end end end end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index 605abfabdab..a2410068845 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -1,7 +1,7 @@ module Gitlab module GithubImport class ProjectCreator - attr_reader :repo, :namespace, :current_user, :session_data + attr_reader :repo, :name, :namespace, :current_user, :session_data def initialize(repo, name, namespace, current_user, session_data) @repo = repo @@ -12,24 +12,37 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, - name: @name, - path: @name, + name: name, + path: name, description: repo.description, namespace_id: namespace.id, - visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility, + visibility_level: visibility_level, import_type: "github", import_source: repo.full_name, - import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") + import_url: import_url, + skip_wiki: skip_wiki ).execute + end + + private - # If repo has wiki we'll import it later - if repo.has_wiki? && project - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) - end + def import_url + repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@") + end + + def visibility_level + repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility + end - project + # + # If the GitHub project repository has wiki, we should not create the + # default wiki. Otherwise the GitHub importer will fail because the wiki + # repository already exist. + # + def skip_wiki + repo.has_wiki? end end end diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index 3e5d728f3bc..f8809db21aa 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -5,19 +5,61 @@ module Gitlab def identify(identifier, project, newrev) if identifier.blank? # Local push from gitlab - email = project.commit(newrev).author_email rescue nil - User.find_by(email: email) if email - + identify_using_commit(project, newrev) elsif identifier =~ /\Auser-\d+\Z/ # git push over http - user_id = identifier.gsub("user-", "") - User.find_by(id: user_id) - + identify_using_user(identifier) elsif identifier =~ /\Akey-\d+\Z/ # git push over ssh - key_id = identifier.gsub("key-", "") - Key.find_by(id: key_id).try(:user) + identify_using_ssh_key(identifier) + end + end + + # Tries to identify a user based on a commit SHA. + def identify_using_commit(project, ref) + commit = project.commit(ref) + + return if !commit || !commit.author_email + + email = commit.author_email + + identify_with_cache(:email, email) do + User.find_by(email: email) end end + + # Tries to identify a user based on a user identifier (e.g. "user-123"). + def identify_using_user(identifier) + user_id = identifier.gsub("user-", "") + + identify_with_cache(:user, user_id) do + User.find_by(id: user_id) + end + end + + # Tries to identify a user based on an SSH key identifier (e.g. "key-123"). + def identify_using_ssh_key(identifier) + key_id = identifier.gsub("key-", "") + + identify_with_cache(:ssh_key, key_id) do + User.find_by_ssh_key_id(key_id) + end + end + + def identify_with_cache(category, key) + if identification_cache[category].key?(key) + identification_cache[category][key] + else + identification_cache[category][key] = yield + end + end + + def identification_cache + @identification_cache ||= { + email: {}, + user: {}, + ssh_key: {} + } + end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 1c42acab9c1..bb9d1080330 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -1,5 +1,8 @@ # Model relationships to be included in the project import/export project_tree: + - :labels + - milestones: + - :events - issues: - :events - notes: @@ -39,9 +42,6 @@ project_tree: - protected_branches: - :merge_access_levels - :push_access_levels - - :labels - - milestones: - - :events - :project_feature # Only include the following attributes for the models specified. diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index c7b3551b84c..35ff134ea19 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -61,11 +61,17 @@ module Gitlab def restore_project return @project unless @tree_hash - project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } @project.update(project_params) @project end + def project_params + @tree_hash.reject do |key, value| + # return params that are not 1 to many or 1 to 1 relations + value.is_a?(Array) || key == key.singularize + end + end + # Given a relation hash containing one or more models and its relationships, # loops through each model and each object from a model type and # and assigns its correspondent attributes hash from +tree_hash+ diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 2f326d00a2f..7e06bd2b0fb 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -51,8 +51,6 @@ module Gitlab user.ldap_block false end - rescue - false end def adapter diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 82cb8cef754..8b38cfaefb6 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -62,6 +62,9 @@ module Gitlab results end end + rescue Net::LDAP::Error => error + Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}") + [] rescue Timeout::Error Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds") [] diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb index d089a2f9b0b..5f67e97fa2a 100644 --- a/lib/gitlab/lfs_token.rb +++ b/lib/gitlab/lfs_token.rb @@ -17,19 +17,13 @@ module Gitlab end end - def generate - token = Devise.friendly_token(TOKEN_LENGTH) - + def token Gitlab::Redis.with do |redis| + token = redis.get(redis_key) + token ||= Devise.friendly_token(TOKEN_LENGTH) redis.set(redis_key, token, ex: EXPIRY_TIME) - end - token - end - - def value - Gitlab::Redis.with do |redis| - redis.get(redis_key) + token end end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 69c4ef721d5..3faab937726 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -11,13 +11,6 @@ module Gitlab DEFAULT_REDIS_URL = 'redis://localhost:6379' CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__) - # To be thread-safe we must be careful when writing the class instance - # variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two - # mutexes to prevent deadlock. - RAW_CONFIG_MUTEX = Mutex.new - POOL_MUTEX = Mutex.new - private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX - class << self # Do NOT cache in an instance variable. Result may be mutated by caller. def params @@ -31,24 +24,19 @@ module Gitlab end def with - if @pool.nil? - POOL_MUTEX.synchronize do - @pool = ConnectionPool.new { ::Redis.new(params) } - end - end + @pool ||= ConnectionPool.new { ::Redis.new(params) } @pool.with { |redis| yield redis } end def _raw_config return @_raw_config if defined?(@_raw_config) - RAW_CONFIG_MUTEX.synchronize do - begin - @_raw_config = File.read(CONFIG_FILE).freeze - rescue Errno::ENOENT - @_raw_config = false - end + begin + @_raw_config = File.read(CONFIG_FILE).freeze + rescue Errno::ENOENT + @_raw_config = false end + @_raw_config end end diff --git a/lib/gitlab/sidekiq_middleware/arguments_logger.rb b/lib/gitlab/sidekiq_middleware/arguments_logger.rb index 7813091ec7b..82a59a7a87e 100644 --- a/lib/gitlab/sidekiq_middleware/arguments_logger.rb +++ b/lib/gitlab/sidekiq_middleware/arguments_logger.rb @@ -2,7 +2,7 @@ module Gitlab module SidekiqMiddleware class ArgumentsLogger def call(worker, job, queue) - Sidekiq.logger.info "arguments: #{job['args']}" + Sidekiq.logger.info "arguments: #{JSON.dump(job['args'])}" yield end end diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake deleted file mode 100644 index 3bfe999ae74..00000000000 --- a/lib/tasks/flog.rake +++ /dev/null @@ -1,25 +0,0 @@ -desc 'Code complexity analyze via flog' -task :flog do - output = %x(bundle exec flog -m app/ lib/gitlab) - exit_code = 0 - minimum_score = 70 - output = output.lines - - # Skip total complexity score - output.shift - - # Skip some trash info - output.shift - - output.each do |line| - score, method = line.split(" ") - score = score.to_i - - if score > minimum_score - exit_code = 1 - puts "High complexity in #{method}. Score: #{score}" - end - end - - exit exit_code -end diff --git a/public/deploy.html b/public/deploy.html index 142472b6c35..49ec4ac5ce1 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -2,6 +2,11 @@ <html> <head> <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"> + <meta name="refresh" content="60"> + <meta name="retry-after" content="100"> + <meta name="robots" content="noindex, nofollow, noarchive, nostore"> + <meta name="cache-control" content="no-cache, no-store"> + <meta name="pragma" content="no-cache"> <title>Deploy in progress</title> <style> body { @@ -61,4 +66,4 @@ <p>Please contact your GitLab administrator if this problem persists.</p> </div> </body> -</html> +</html>
\ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt index 334f4c03533..7d69fad59d1 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -23,7 +23,7 @@ Disallow: /groups/*/edit Disallow: /users # Global snippets -Disallow: /s +Disallow: /s/ Disallow: /snippets/new Disallow: /snippets/*/edit Disallow: /snippets/*/raw diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 76b2178c79c..1eaafdce389 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -16,21 +16,6 @@ retry() { } if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then - mkdir -p vendor/apt - - # Install phantomjs package - pushd vendor/apt - PHANTOMJS_FILE="phantomjs-$PHANTOMJS_VERSION-linux-x86_64" - if [ ! -d "$PHANTOMJS_FILE" ]; then - curl -q -L "https://s3.amazonaws.com/gitlab-build-helpers/$PHANTOMJS_FILE.tar.bz2" | tar jx - fi - cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/" - popd - - # Try to install packages - retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip' - cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml sed -i 's/password:.*/password:/g' config/database.yml @@ -39,7 +24,7 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - export FLAGS=(--path vendor --retry 3) + export FLAGS=(--path vendor --retry 3 --quiet) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index c34475976c6..a0870891cf4 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -2,9 +2,10 @@ require 'spec_helper' describe Groups::GroupMembersController do let(:user) { create(:user) } - let(:group) { create(:group) } describe '#index' do + let(:group) { create(:group) } + before do group.add_owner(user) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @@ -86,10 +87,10 @@ describe Groups::GroupMembersController do context 'when member is not found' do before { sign_in(user) } - it 'returns 403' do + it 'returns 404' do delete :leave, group_id: group - expect(response).to have_http_status(403) + expect(response).to have_http_status(404) end end diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index d687dea3c3b..709006a3601 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -20,10 +20,7 @@ describe Projects::Boards::ListsController do end it 'returns a list of board lists' do - board = project.create_board - create(:backlog_list, board: board) create(:list, board: board) - create(:done_list, board: board) read_board_list user: user diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index fbe8758dda7..b9d9117c928 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Projects::GroupLinksController do - let(:project) { create(:project, :private) } let(:group) { create(:group, :private) } + let(:group2) { create(:group, :private) } + let(:project) { create(:project, :private, group: group2) } let(:user) { create(:user) } before do @@ -46,5 +47,39 @@ describe Projects::GroupLinksController do expect(group.shared_projects).not_to include project end end + + context 'when project group id equal link group id' do + before do + post(:create, namespace_id: project.namespace.to_param, + project_id: project.to_param, + link_group_id: group2.id, + link_group_access: ProjectGroupLink.default_access) + end + + it 'does not share project with selected group' do + expect(group2.shared_projects).not_to include project + end + + it 'redirects to project group links page' do + expect(response).to redirect_to( + namespace_project_group_links_path(project.namespace, project) + ) + end + end + + context 'when link group id is not present' do + before do + post(:create, namespace_id: project.namespace.to_param, + project_id: project.to_param, + link_group_access: ProjectGroupLink.default_access) + end + + it 'redirects to project group links page' do + expect(response).to redirect_to( + namespace_project_group_links_path(project.namespace, project) + ) + expect(flash[:alert]).to eq('Please select a group.') + 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 94c9edc91fe..742edd8ba3d 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -644,6 +644,20 @@ describe Projects::MergeRequestsController do end end + context 'POST remove_wip' do + it 'removes the wip status' do + merge_request.title = merge_request.wip_title + merge_request.save + + post :remove_wip, + namespace_id: merge_request.project.namespace.to_param, + project_id: merge_request.project.to_param, + id: merge_request.iid + + expect(merge_request.reload.title).to eq(merge_request.wipless_title) + end + end + context 'POST resolve_conflicts' do let(:json_response) { JSON.parse(response.body) } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 5e2a8cf3849..074f85157de 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do context 'when member is not found' do before { sign_in(user) } - it 'returns 403' do + it 'returns 404' do delete :leave, namespace_id: project.namespace, project_id: project - expect(response).to have_http_status(403) + expect(response).to have_http_status(404) end end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 2fe3c263524..38e02a46626 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -8,7 +8,7 @@ describe Projects::RepositoriesController do it 'responds with redirect in correct format' do get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip" - expect(response.content_type).to start_with 'text/html' + expect(response.header["Content-Type"]).to start_with('text/html') expect(response).to be_redirect end end diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 7b3a26d7ca7..19a152bcb05 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -13,7 +13,7 @@ describe Projects::TemplatesController do end before do - project.team.add_user(user, Gitlab::Access::MASTER) + project.add_user(user, Gitlab::Access::MASTER) project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index b0f740f48f7..da0fdce39db 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -63,6 +63,28 @@ describe ProjectsController do end end + context "project with broken repo" do + let(:empty_project) { create(:project_broken_repo, :public) } + + before { sign_in(user) } + + User.project_views.keys.each do |project_view| + context "with #{project_view} view set" do + before do + user.update_attributes(project_view: project_view) + + get :show, namespace_id: empty_project.namespace.path, id: empty_project.path + end + + it "renders the empty project view" do + allow(Project).to receive(:repo).and_raise(Gitlab::Git::Repository::NoRepository) + + expect(response).to render_template('projects/no_repo') + end + end + end + end + context "rendering default project view" do render_views diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 8f27e616c3e..48d69377461 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -109,6 +109,44 @@ describe SessionsController do end end + context 'when the user is on their last attempt' do + before do + user.update(failed_attempts: User.maximum_attempts.pred) + end + + context 'when OTP is valid' do + it 'authenticates correctly' do + authenticate_2fa(otp_attempt: user.current_otp) + + expect(subject.current_user).to eq user + end + end + + context 'when OTP is invalid' do + before { authenticate_2fa(otp_attempt: 'invalid') } + + it 'does not authenticate' do + expect(subject.current_user).not_to eq user + end + + it 'warns about invalid login' do + expect(response).to set_flash.now[:alert] + .to /Invalid Login or password/ + end + + it 'locks the user' do + expect(user.reload).to be_access_locked + end + + it 'keeps the user locked on future login attempts' do + post(:create, user: { login: user.username, password: user.password }) + + expect(response) + .to set_flash.now[:alert].to /Invalid Login or password/ + end + end + end + context 'when another user does not have 2FA enabled' do let(:another_user) { create(:user) } diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb index cf3659ba275..1ddb305a8af 100644 --- a/spec/factories/project_members.rb +++ b/spec/factories/project_members.rb @@ -4,24 +4,9 @@ FactoryGirl.define do project master - trait :guest do - access_level ProjectMember::GUEST - end - - trait :reporter do - access_level ProjectMember::REPORTER - end - - trait :developer do - access_level ProjectMember::DEVELOPER - end - - trait :master do - access_level ProjectMember::MASTER - end - - trait :owner do - access_level ProjectMember::OWNER - end + trait(:guest) { access_level ProjectMember::GUEST } + trait(:reporter) { access_level ProjectMember::REPORTER } + trait(:developer) { access_level ProjectMember::DEVELOPER } + trait(:master) { access_level ProjectMember::MASTER } end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index fb84ba07d25..873d3fcb5af 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -27,6 +27,14 @@ FactoryGirl.define do end end + trait :broken_repo do + after(:create) do |project| + project.create_repository + + FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs')) + end + end + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED @@ -56,6 +64,13 @@ FactoryGirl.define do empty_repo end + # Project with broken repository + # + # Project with an invalid repository state + factory :project_broken_repo, parent: :empty_project do + broken_repo + end + # Project with test repository # # Test repository source can be found at @@ -106,6 +121,8 @@ FactoryGirl.define do factory :project_with_board, parent: :empty_project do after(:create) do |project| project.create_board + project.board.lists.create(list_type: :backlog) + project.board.lists.create(list_type: :done) end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 19941978c5f..26ea06e002b 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -4,15 +4,11 @@ describe 'Issue Boards', feature: true, js: true do include WaitForAjax include WaitForVueResource - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project_with_board, :public) } let(:user) { create(:user) } let!(:user2) { create(:user) } before do - project.create_board - project.board.lists.create(list_type: :backlog) - project.board.lists.create(list_type: :done) - project.team << [user, :master] project.team << [user2, :master] @@ -62,6 +58,7 @@ describe 'Issue Boards', feature: true, js: true do let(:bug) { create(:label, project: project, name: 'Bug') } let!(:backlog) { create(:label, project: project, name: 'Backlog') } let!(:done) { create(:label, project: project, name: 'Done') } + let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } let!(:list1) { create(:list, board: project.board, label: planning, position: 0) } let!(:list2) { create(:list, board: project.board, label: development, position: 1) } @@ -75,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) } let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) } let!(:issue8) { create(:closed_issue, project: project) } - let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) } + let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) } before do visit namespace_project_board_path(project.namespace, project) @@ -441,6 +438,57 @@ describe 'Issue Boards', feature: true, js: true do wait_for_empty_boards((2..4)) end + it 'filters by label with space after reload' do + page.within '.issues-filters' do + click_button('Label') + wait_for_ajax + + page.within '.dropdown-menu-labels' do + click_link(accepting.title) + wait_for_vue_resource(spinner: false) + find('.dropdown-menu-close').click + end + end + + # Test after reload + page.evaluate_script 'window.location.reload()' + + wait_for_vue_resource + + page.within(find('.board', match: :first)) do + expect(page.find('.board-header')).to have_content('1') + expect(page).to have_selector('.card', count: 1) + end + + page.within(find('.board:nth-child(2)')) do + expect(page.find('.board-header')).to have_content('0') + expect(page).to have_selector('.card', count: 0) + end + end + + it 'removes filtered labels' do + wait_for_vue_resource + + page.within '.labels-filter' do + click_button('Label') + wait_for_ajax + + page.within '.dropdown-menu-labels' do + click_link(testing.title) + wait_for_vue_resource(spinner: false) + end + + expect(page).to have_css('input[name="label_name[]"]', visible: false) + + page.within '.dropdown-menu-labels' do + click_link(testing.title) + wait_for_vue_resource(spinner: false) + end + + expect(page).not_to have_css('input[name="label_name[]"]', visible: false) + end + end + it 'infinite scrolls list with label filter' do 50.times do create(:labeled_issue, project: project, labels: [testing]) diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index fd5fbaf2af4..7fa0c95cae2 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -5,13 +5,43 @@ feature 'Contributions Calendar', js: true, feature: true do let(:contributed_project) { create(:project, :public) } - before do - login_as :user + # Ex/ Sunday Jan 1, 2016 + date_format = '%A %b %-d, %Y' + + issue_title = 'Bug in old browser' + issue_params = { title: issue_title } + + def get_cell_color_selector(contributions) + contribution_cell = '.user-contrib-cell' + activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77'] + activity_colors_index = 0 + + if contributions > 0 && contributions < 10 + activity_colors_index = 1 + elsif contributions >= 10 && contributions < 20 + activity_colors_index = 2 + elsif contributions >= 20 && contributions < 30 + activity_colors_index = 3 + elsif contributions >= 30 + activity_colors_index = 4 + end + + "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']" + end - issue_params = { title: 'Bug in old browser' } - Issues::CreateService.new(contributed_project, @user, issue_params).execute + def get_cell_date_selector(contributions, date) + contribution_text = 'No contributions' - # Push code contribution + if contributions === 1 + contribution_text = '1 contribution' + elsif contributions > 1 + contribution_text = "#{contributions} contributions" + end + + "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']" + end + + def push_code_contribution push_params = { project: contributed_project, action: Event::PUSHED, @@ -20,7 +50,10 @@ feature 'Contributions Calendar', js: true, feature: true do } Event.create(push_params) + end + before do + login_as :user visit @user.username wait_for_ajax end @@ -29,11 +62,71 @@ feature 'Contributions Calendar', js: true, feature: true do expect(page).to have_css('.js-contrib-calendar') end - it 'displays calendar activity log', js: true do - expect(find('.content_list .event-note')).to have_content "Bug in old browser" + describe '1 calendar activity' do + before do + Issues::CreateService.new(contributed_project, @user, issue_params).execute + visit @user.username + wait_for_ajax + end + + it 'displays calendar activity log', js: true do + expect(find('.content_list .event-note')).to have_content issue_title + end + + it 'displays calendar activity square color for 1 contribution', js: true do + expect(page).to have_selector(get_cell_color_selector(1), count: 1) + end + + it 'displays calendar activity square on the correct date', js: true do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + end end - it 'displays calendar activity square color', js: true do - expect(page).to have_selector('.user-contrib-cell[fill=\'#acd5f2\']', count: 1) + describe '10 calendar activities' do + before do + (0..9).each do |i| + push_code_contribution() + end + + visit @user.username + wait_for_ajax + end + + it 'displays calendar activity square color for 10 contributions', js: true do + expect(page).to have_selector(get_cell_color_selector(10), count: 1) + end + + it 'displays calendar activity square on the correct date', js: true do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(10, today), count: 1) + end + end + + describe 'calendar activity on two days' do + before do + push_code_contribution() + + Timecop.freeze(Date.yesterday) + Issues::CreateService.new(contributed_project, @user, issue_params).execute + Timecop.return + + visit @user.username + wait_for_ajax + end + + it 'displays calendar activity squares for both days', js: true do + expect(page).to have_selector(get_cell_color_selector(1), count: 2) + end + + it 'displays calendar activity square for yesterday', js: true do + yesterday = Date.yesterday.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + end + + it 'displays calendar activity square for today', js: true do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + end end end diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb index ca7f73e24cc..33dfd0d5b62 100644 --- a/spec/features/compare_spec.rb +++ b/spec/features/compare_spec.rb @@ -12,15 +12,16 @@ describe "Compare", js: true do describe "branches" do it "pre-populates fields" do - expect(page.find_field("from").value).to eq("master") + expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master") + expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master") end it "compares branches" do - fill_in "from", with: "fea" - find("#from").click + select_using_dropdown "from", "feature" + expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature") - click_link "feature" - expect(page.find_field("from").value).to eq("feature") + select_using_dropdown "to", "binary-encoding" + expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding") click_button "Compare" expect(page).to have_content "Commits" @@ -29,14 +30,21 @@ describe "Compare", js: true do describe "tags" do it "compares tags" do - fill_in "from", with: "v1.0" - find("#from").click + select_using_dropdown "from", "v1.0.0" + expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0") - click_link "v1.0.0" - expect(page.find_field("from").value).to eq("v1.0.0") + select_using_dropdown "to", "v1.1.0" + expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0") click_button "Compare" expect(page).to have_content "Commits" end end + + def select_using_dropdown(dropdown_type, selection) + dropdown = find(".js-compare-#{dropdown_type}-dropdown") + dropdown.find(".compare-dropdown-toggle").click + dropdown.fill_in("Filter by branch/tag", with: selection) + click_link selection + end end diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb new file mode 100644 index 00000000000..62937688c22 --- /dev/null +++ b/spec/features/dashboard/snippets_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'Dashboard snippets', feature: true do + context 'when the project has snippets' do + let(:project) { create(:empty_project, :public) } + let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } + before do + allow(Snippet).to receive(:default_per_page).and_return(1) + login_as(project.owner) + visit dashboard_snippets_path + end + + it_behaves_like 'paginated snippets' + end +end diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index 3fb1cb37544..9b54b5301e5 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do click_link 'No Milestone' + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_selector('.issue', count: 1) end @@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do click_link 'Any Milestone' + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) expect(page).to have_selector('.issue', count: 2) end @@ -39,6 +41,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do click_link milestone.title end + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_selector('.issue', count: 1) end end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 8863554ee91..6c938bdead8 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -68,7 +68,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do context 'expanding a diff for a renamed file' do before do - large_diff_renamed.find('.nothing-here-block').click + large_diff_renamed.find('.click-to-expand').click wait_for_ajax end @@ -87,7 +87,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do context 'expanding a large diff' do before do - click_link('large_diff.md') + # Wait for diffs + find('.file-title', match: :first) + # Click `large_diff.md` title + all('.file-title')[1].click wait_for_ajax end @@ -128,7 +131,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do context 'expanding the diff' do before do - click_link('large_diff.md') + # Wait for diffs + find('.file-title', match: :first) + # Click `large_diff.md` title + all('.file-title')[1].click wait_for_ajax end @@ -146,7 +152,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do end context 'collapsing an expanded diff' do - before { click_link('small_diff.md') } + before do + # Wait for diffs + find('.file-title', match: :first) + # Click `small_diff.md` title + all('.file-title')[3].click + end it 'hides the diff content' do expect(small_diff).not_to have_selector('.code') @@ -154,7 +165,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do end context 're-expanding the same diff' do - before { click_link('small_diff.md') } + before do + # Wait for diffs + find('.file-title', match: :first) + # Click `small_diff.md` title + all('.file-title')[3].click + end it 'shows the diff content' do expect(small_diff).to have_selector('.code') @@ -231,7 +247,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do end context 'collapsing an expanded diff' do - before { click_link('small_diff.md') } + before do + # Wait for diffs + find('.file-title', match: :first) + # Click `small_diff.md` title + all('.file-title')[3].click + end it 'hides the diff content' do expect(small_diff).not_to have_selector('.code') @@ -239,7 +260,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do end context 're-expanding the same diff' do - before { click_link('small_diff.md') } + before do + # Wait for diffs + find('.file-title', match: :first) + # Click `small_diff.md` title + all('.file-title')[3].click + end it 'shows the diff content' do expect(small_diff).to have_selector('.code') diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 908b18e5339..0253629f753 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -1,10 +1,10 @@ require 'rails_helper' -feature 'Issue filtering by Labels', feature: true do +feature 'Issue filtering by Labels', feature: true, js: true do include WaitForAjax let(:project) { create(:project, :public) } - let!(:user) { create(:user)} + let!(:user) { create(:user) } let!(:label) { create(:label, project: project) } before do @@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do visit namespace_project_issues_path(project.namespace, project) end - context 'filter by label bug', js: true do + context 'filter by label bug' do before do - page.find('.js-label-select').click - wait_for_ajax - execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax + select_labels('bug') end - it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do + it 'apply the filter' do expect(page).to have_content "Bugfix1" expect(page).to have_content "Bugfix2" - end - - it 'does not show "Feature1" in issues list' do expect(page).not_to have_content "Feature1" - end - - it 'shows label "bug" in filtered-labels' do expect(find('.filtered-labels')).to have_content "bug" - end - - it 'does not show label "feature" and "enhancement" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "enhancement" - end - it 'removes label "bug"' do find('.js-label-filter-remove').click wait_for_ajax expect(find('.filtered-labels', visible: false)).to have_no_content "bug" end end - context 'filter by label feature', js: true do + context 'filter by label feature' do before do - page.find('.js-label-select').click - wait_for_ajax - execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()") - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax + select_labels('feature') end - it 'shows issue "Feature1" in issues list' do + it 'applies the filter' do expect(page).to have_content "Feature1" - end - - it 'does not show "Bugfix1" and "Bugfix2" in issues list' do expect(page).not_to have_content "Bugfix2" expect(page).not_to have_content "Bugfix1" - end - - it 'shows label "feature" in filtered-labels' do expect(find('.filtered-labels')).to have_content "feature" - end - - it 'does not show label "bug" and "enhancement" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "enhancement" end end - context 'filter by label enhancement', js: true do + context 'filter by label enhancement' do before do - page.find('.js-label-select').click - wait_for_ajax - execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax + select_labels('enhancement') end - it 'shows issue "Bugfix2" in issues list' do + it 'applies the filter' do expect(page).to have_content "Bugfix2" - end - - it 'does not show "Feature1" and "Bugfix1" in issues list' do expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Bugfix1" - end - - it 'shows label "enhancement" in filtered-labels' do expect(find('.filtered-labels')).to have_content "enhancement" - end - - it 'does not show label "feature" and "bug" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "feature" end end - context 'filter by label enhancement or feature', js: true do + context 'filter by label enhancement and bug in issues list' do before do - page.find('.js-label-select').click - wait_for_ajax - execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") - execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()") - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax + select_labels('bug', 'enhancement') end - it 'does not show "Bugfix1" or "Feature1" in issues list' do - expect(page).not_to have_content "Bugfix1" + it 'applies the filters' do + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content "Bugfix2" expect(page).not_to have_content "Feature1" - end - - it 'shows label "enhancement" and "feature" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "bug" expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).to have_content "feature" - end - - it 'does not show label "bug" in filtered-labels' do - expect(find('.filtered-labels')).not_to have_content "bug" - end + expect(find('.filtered-labels')).not_to have_content "feature" - it 'removes label "enhancement"' do find('.js-label-filter-remove', match: :first).click wait_for_ajax - expect(find('.filtered-labels')).to have_no_content "enhancement" - end - end - - context 'filter by label enhancement and bug in issues list', js: true do - before do - page.find('.js-label-select').click - wait_for_ajax - execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") - execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - end - it 'shows issue "Bugfix2" in issues list' do expect(page).to have_content "Bugfix2" - end - - it 'does not show "Feature1"' do expect(page).not_to have_content "Feature1" - end - - it 'shows label "bug" and "enhancement" in filtered-labels' do - expect(find('.filtered-labels')).to have_content "bug" + expect(page).not_to have_content "Bugfix1" + expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).to have_content "enhancement" - end - - it 'does not show label "feature" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "feature" end end - context 'remove filtered labels', js: true do + context 'remove filtered labels' do before do page.within '.labels-filter' do click_button 'Label' @@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do end end - context 'dropdown filtering', js: true do + context 'dropdown filtering' do it 'filters by label name' do page.within '.labels-filter' do click_button 'Label' @@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do end end end + + def select_labels(*labels) + page.find('.js-label-select').click + wait_for_ajax + labels.each do |label| + execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()") + end + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index d1501c9791a..78208aed46d 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -7,15 +7,15 @@ describe 'Filter issues', feature: true do let!(:user) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } - let!(:issue1) { create(:issue, project: project) } let!(:wontfix) { create(:label, project: project, title: "Won't fix") } before do project.team << [user, :master] login_as(user) + create(:issue, project: project) end - describe 'Filter issues for assignee from issues#index' do + describe 'for assignee from issues#index' do before do visit namespace_project_issues_path(project.namespace, project) @@ -45,7 +45,7 @@ describe 'Filter issues', feature: true do end end - describe 'Filter issues for milestone from issues#index' do + describe 'for milestone from issues#index' do before do visit namespace_project_issues_path(project.namespace, project) @@ -75,7 +75,7 @@ describe 'Filter issues', feature: true do end end - describe 'Filter issues for label from issues#index', js: true do + describe 'for label from issues#index', js: true do before do visit namespace_project_issues_path(project.namespace, project) find('.js-label-select').click @@ -96,9 +96,9 @@ describe 'Filter issues', feature: true do wait_for_ajax page.within '.labels-filter' do - expect(page).to have_content 'No Label' + expect(page).to have_content 'Labels' end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') end it 'filters by a label' do @@ -110,29 +110,37 @@ describe 'Filter issues', feature: true do end it "filters by `won't fix` and another label" do - find('.dropdown-menu-labels a', text: label.title).click page.within '.labels-filter' do - expect(page).to have_content wontfix.title click_link wontfix.title + expect(page).to have_content wontfix.title + click_link label.title end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title) + + expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more") end it "filters by `won't fix` label followed by another label after page load" do - find('.dropdown-menu-labels a', text: wontfix.title).click - # Close label dropdown to load + page.within '.labels-filter' do + click_link wontfix.title + expect(page).to have_content wontfix.title + end + find('body').click + expect(find('.filtered-labels')).to have_content(wontfix.title) find('.js-label-select').click wait_for_ajax find('.dropdown-menu-labels a', text: label.title).click - # Close label dropdown to load + find('body').click + + expect(find('.filtered-labels')).to have_content(wontfix.title) expect(find('.filtered-labels')).to have_content(label.title) find('.js-label-select').click wait_for_ajax + expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active') expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active') end @@ -146,7 +154,7 @@ describe 'Filter issues', feature: true do end end - describe 'Filter issues for assignee and label from issues#index' do + describe 'for assignee and label from issues#index' do before do visit namespace_project_issues_path(project.namespace, project) @@ -226,6 +234,7 @@ describe 'Filter issues', feature: true do it 'filters by text and label' 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) end @@ -236,6 +245,7 @@ describe 'Filter issues', feature: true do end 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) end @@ -244,6 +254,7 @@ describe 'Filter issues', feature: true do it 'filters by text and milestone' 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) end @@ -253,6 +264,7 @@ describe 'Filter issues', feature: true do click_link '8' 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) end @@ -261,6 +273,7 @@ describe 'Filter issues', feature: true do it 'filters by text and assignee' 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) end @@ -270,6 +283,7 @@ describe 'Filter issues', feature: true do click_link user.name 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) end @@ -278,6 +292,7 @@ describe 'Filter issues', feature: true do it 'filters by text and author' 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) end @@ -287,6 +302,7 @@ describe 'Filter issues', feature: true do click_link user.name 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) end @@ -315,6 +331,7 @@ describe 'Filter issues', feature: true do find('.dropdown-menu-close-icon').click 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) end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb new file mode 100644 index 00000000000..8771cc8e157 --- /dev/null +++ b/spec/features/issues/form_spec.rb @@ -0,0 +1,119 @@ +require 'rails_helper' + +describe 'New/edit issue', feature: true, js: true do + let!(:project) { create(:project) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:label2) { create(:label, project: project) } + let!(:issue) { create(:issue, project: project, assignee: user, milestone: milestone) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'new issue' do + before do + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'allows user to create new issue' do + fill_in 'issue_title', with: 'title' + fill_in 'issue_description', with: 'title' + + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + + context 'edit issue' do + before do + visit edit_namespace_project_issue_path(project.namespace, project, issue) + end + + it 'allows user to update issue' do + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + + page.within '.js-user-search' do + expect(page).to have_content user.name + end + + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end +end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 7773c486b4e..055210399a7 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -55,7 +55,7 @@ feature 'issue move to another project' do first('.select2-choice').click end - fill_in('s2id_autogen2_search', with: new_project_search.name) + fill_in('s2id_autogen1_search', with: new_project_search.name) page.within '.select2-drop' do expect(page).to have_content(new_project_search.name) diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 105629c485a..3f2da1c380c 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -25,32 +25,88 @@ feature 'Issues > User uses slash commands', feature: true, js: true do describe 'adding a due date from note' do let(:issue) { create(:issue, project: project) } - it 'does not create a note, and sets the due date accordingly' do - write_note("/due 2016-08-28") + context 'when the current user can update the due date' do + it 'does not create a note, and sets the due date accordingly' 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).not_to have_content '/due 2016-08-28' + expect(page).to have_content 'Your commands have been executed!' - issue.reload + issue.reload - expect(issue.due_date).to eq Date.new(2016, 8, 28) + expect(issue.due_date).to eq Date.new(2016, 8, 28) + end + end + + context 'when the current user cannot update the due date' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'does not create a note, and sets the due date accordingly' 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!' + + issue.reload + + expect(issue.due_date).to be_nil + end end end describe 'removing a due date from note' do let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) } - it 'does not create a note, and removes the due date accordingly' do - expect(issue.due_date).to eq Date.new(2016, 8, 28) + context 'when the current user can update the due date' do + it 'does not create a note, and removes the due date accordingly' do + expect(issue.due_date).to eq Date.new(2016, 8, 28) + + write_note("/remove_due_date") + + expect(page).not_to have_content '/remove_due_date' + expect(page).to have_content 'Your commands have been executed!' + + issue.reload - write_note("/remove_due_date") + expect(issue.due_date).to be_nil + end + end + + context 'when the current user cannot update the due date' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'does not create a note, and sets the due date accordingly' 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!' + + issue.reload - expect(page).not_to have_content '/remove_due_date' - expect(page).to have_content 'Your commands have been executed!' + expect(issue.due_date).to eq Date.new(2016, 8, 28) + end + end + end + + describe 'toggling the WIP prefix from the title from note' do + let(:issue) { create(:issue, project: project) } - issue.reload + it 'does not recognize the command nor create a note' do + write_note("/wip") - expect(issue.due_date).to be_nil + expect(page).not_to have_content '/wip' end end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 22359c8f938..b504329656f 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -51,9 +51,8 @@ describe 'Issues', feature: true do expect(page).to have_content "Assignee #{@user.name}" - first('#s2id_issue_assignee_id').click - sleep 2 # wait for ajax stuff to complete - first('.user-result').click + first('.js-user-search').click + click_link 'Unassigned' click_button 'Save changes' @@ -369,6 +368,24 @@ describe 'Issues', feature: true do end end + describe 'update labels from issue#show', js: true do + let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } + let!(:label) { create(:label, project: project) } + + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'will not send ajax request when no data is changed' do + page.within '.labels' do + click_link 'Edit' + first('.dropdown-menu-close').click + + expect(page).not_to have_selector('.block-loading') + end + end + end + describe 'update assignee from issue#show' do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index bb0bb590a46..d917d5950ec 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) filter_by_milestone(Milestone::None.title) + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end @@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end @@ -61,6 +63,7 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) filter_by_milestone(milestone.title) + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb new file mode 100644 index 00000000000..7594cbf54e8 --- /dev/null +++ b/spec/features/merge_requests/form_spec.rb @@ -0,0 +1,273 @@ +require 'rails_helper' + +describe 'New/edit merge request', feature: true, js: true do + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:fork_project) { create(:project, forked_from_project: project) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:label2) { create(:label, project: project) } + + before do + project.team << [user, :master] + end + + context 'owned projects' do + before do + login_as(user) + end + + context 'new merge request' do + before do + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end + + it 'creates new merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit merge request' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + + context 'edit merge request' do + before do + merge_request = create(:merge_request, + source_project: project, + target_project: project, + source_branch: 'fix', + target_branch: 'master' + ) + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'updates merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + page.within '.js-label-select' do + expect(page).to have_content label.title + end + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + end + + context 'forked project' do + before do + fork_project.team << [user, :master] + login_as(user) + end + + context 'new merge request' do + before do + visit new_namespace_project_merge_request_path( + fork_project.namespace, + fork_project, + merge_request: { + source_project_id: fork_project.id, + target_project_id: project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end + + it 'creates new merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit merge request' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + + context 'edit merge request' do + before do + merge_request = create(:merge_request, + source_project: fork_project, + target_project: project, + source_branch: 'fix', + target_branch: 'master' + ) + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should update merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + page.within '.js-label-select' do + expect(page).to have_content label.title + end + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + 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 22d9e42119d..23cee891bac 100644 --- a/spec/features/merge_requests/merge_request_versions_spec.rb +++ b/spec/features/merge_requests/merge_request_versions_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' feature 'Merge Request versions', js: true, feature: true do + let(:merge_request) { create(:merge_request, importing: true) } + let(:project) { merge_request.source_project } + before do login_as :admin - merge_request = create(:merge_request, importing: true) merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') - project = merge_request.source_project visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -47,6 +48,16 @@ feature 'Merge Request versions', js: true, feature: true do end end + it 'has a path with comparison context' do + expect(page).to have_current_path diffs_namespace_project_merge_request_path( + project.namespace, + project, + merge_request.iid, + diff_id: 2, + start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' + ) + end + it 'should have correct value in the compare dropdown' do page.within '.mr-version-compare-dropdown' do expect(page).to have_content 'version 1' @@ -61,10 +72,6 @@ feature 'Merge Request versions', js: true, feature: true do expect(page).to have_content '4 changed files with 15 additions and 6 deletions' end - it 'show diff between new and old version' do - expect(page).to have_content '4 changed files with 15 additions and 6 deletions' - end - it 'should return to latest version when "Show latest version" button is clicked' do click_link 'Show latest version' page.within '.mr-version-dropdown' do 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 22d9d1b9fd5..cb3cea3fd51 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -14,21 +14,66 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } end - describe 'adding a due date from note' do + describe 'merge-request-only commands' do before do project.team << [user, :master] login_with(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - + after do wait_for_ajax end - it 'does not recognize the command nor create a note' do - write_note("/due 2016-08-28") + describe 'toggling the WIP prefix in the title from note' do + context 'when the current user can toggle the WIP prefix' do + it 'adds the WIP: prefix to the title' do + write_note("/wip") + + expect(page).not_to have_content '/wip' + expect(page).to have_content 'Your commands have been executed!' + + expect(merge_request.reload.work_in_progress?).to eq true + end + + it 'removes the WIP: prefix from the title' do + merge_request.title = merge_request.wip_title + merge_request.save + write_note("/wip") + + expect(page).not_to have_content '/wip' + expect(page).to have_content 'Your commands have been executed!' + + expect(merge_request.reload.work_in_progress?).to eq false + end + end + + context 'when the current user cannot toggle the WIP prefix' 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 change the WIP prefix' do + write_note("/wip") + + expect(page).not_to have_content '/wip' + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload.work_in_progress?).to eq false + 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') - expect(page).not_to have_content '/due 2016-08-28' + expect(page).not_to have_content '/due 2016-08-28' + end end end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index f1c522155d3..5d7247e2a62 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -240,6 +240,18 @@ describe 'Comments', feature: true do is_expected.to have_css('.notes_holder .note', count: 1) is_expected.to have_button('Reply...') end + + it 'adds code to discussion' do + click_button 'Reply...' + + page.within(first('.js-discussion-note-form')) do + fill_in 'note[note]', with: '```{{ test }}```' + + click_button('Comment') + end + + expect(page).to have_content('{{ test }}') + end end end end diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index 3b20d38c520..eb1050d21c6 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -1,18 +1,57 @@ require 'rails_helper' -describe 'Profile > SSH Keys', feature: true do +feature 'Profile > SSH Keys', feature: true do let(:user) { create(:user) } before do login_as(user) - visit profile_keys_path end - describe 'User adds an SSH key' do - it 'auto-populates the title', js: true do + describe 'User adds a key' do + before do + visit profile_keys_path + end + + scenario 'auto-populates the title', js: true do fill_in('Key', with: attributes_for(:key).fetch(:key)) expect(find_field('Title').value).to eq 'dummy@gitlab.com' end + + scenario 'saves the new key' do + attrs = attributes_for(:key) + + fill_in('Key', with: attrs[:key]) + fill_in('Title', with: attrs[:title]) + click_button('Add key') + + expect(page).to have_content("Title: #{attrs[:title]}") + expect(page).to have_content(attrs[:key]) + end + end + + scenario 'User sees their keys' do + key = create(:key, user: user) + visit profile_keys_path + + expect(page).to have_content(key.title) + end + + scenario 'User removes a key via the key index' do + create(:key, user: user) + visit profile_keys_path + + click_link('Remove') + + expect(page).to have_content('Your SSH keys (0)') + end + + scenario 'User removes a key via its details page' do + key = create(:key, user: user) + visit profile_key_path(key) + + click_link('Remove') + + expect(page).to have_content('Your SSH keys (0)') end end diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb new file mode 100644 index 00000000000..012befa7990 --- /dev/null +++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'User uses soft wrap whilst editing file', feature: true, js: true do + 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: 'test_file-name') + editor = find('.file-editor.code') + editor.click + editor.send_keys 'Touch water with paw then recoil in horror chase dog then + run away chase the pig around the house eat owner\'s food, and knock + dish off table head butt cant eat out of my own dish. Cat is love, cat + is life rub face on everything poop on grasses so meow. Playing with + balls of wool flee in terror at cucumber discovered on floor run in + circles tuxedo cats always looking dapper, but attack dog, run away + and pretend to be victim so all of a sudden cat goes crazy, yet chase + laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn + hanging out of own butt jump off balcony, onto stranger\'s head yet + chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish + end + + let(:toggle_button) { find('.soft-wrap-toggle') } + + scenario 'user clicks the "Soft wrap" button and then "No wrap" button' do + wrapped_content_width = get_content_width + toggle_button.click + expect(toggle_button).to have_content 'No wrap' + unwrapped_content_width = get_content_width + expect(unwrapped_content_width).to be < wrapped_content_width + + toggle_button.click + expect(toggle_button).to have_content 'Soft wrap' + expect(get_content_width).to be > unwrapped_content_width + end + + def get_content_width + find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/) + end +end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 09cd6369881..f32834801a0 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -86,14 +86,14 @@ feature 'Import/Export - project import integration test', feature: true, js: tr login_as(normal_user) end - scenario 'non-admin user is not allowed to import a project' do + scenario 'non-admin user is allowed to import a project' do expect(Project.all.count).to be_zero visit new_project_path fill_in :project_path, with: 'test-project-path', visible: true - expect(page).not_to have_content('GitLab export') + expect(page).to have_content('GitLab export') end end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index f76c4fe8b57..cd79c4f512d 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template + preview_template(template_content) save_changes end @@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do end end + context 'user creates an issue using templates, with a prior description' do + let(:prior_description) { 'test issue description' } + let(:template_content) { 'this is a test "bug" template' } + let(:issue) { create(:issue, author: user, assignee: user, project: project) } + + background do + project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) + visit edit_namespace_project_issue_path project.namespace, project, issue + fill_in :'issue[title]', with: 'test issue title' + fill_in :'issue[description]', with: prior_description + end + + scenario 'user selects "bug" template' do + select_template 'bug' + wait_for_ajax + preview_template("#{prior_description}\n\n#{template_content}") + save_changes + end + end + context 'user creates a merge request using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } @@ -55,7 +75,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 + preview_template(template_content) save_changes end end @@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects template' do select_template 'feature-proposal' wait_for_ajax - preview_template + preview_template(template_content) save_changes end end end end - def preview_template + def preview_template(expected_content) click_link 'Preview' - expect(page).to have_content template_content + expect(page).to have_content expected_content end def save_changes diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb index 67811b1048e..6e948b7a616 100644 --- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb +++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb @@ -1,12 +1,10 @@ require 'spec_helper' feature 'Projects > Members > Owner cannot leave project', feature: true do - let(:owner) { create(:user) } let(:project) { create(:project) } background do - project.team << [owner, :owner] - login_as(owner) + login_as(project.owner) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb index 0e54c4fdf20..4ca9272b9c1 100644 --- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb +++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb @@ -1,12 +1,10 @@ require 'spec_helper' feature 'Projects > Members > Owner cannot request access to his project', feature: true do - let(:owner) { create(:user) } let(:project) { create(:project) } background do - project.team << [owner, :owner] - login_as(owner) + login_as(project.owner) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb new file mode 100644 index 00000000000..d37e8ed4699 --- /dev/null +++ b/spec/features/projects/snippets_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe 'Project snippets', feature: true do + context 'when the project has snippets' do + let(:project) { create(:empty_project, :public) } + let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } + before do + allow(Snippet).to receive(:default_per_page).and_return(1) + visit namespace_project_snippets_path(project.namespace, project) + end + + it_behaves_like 'paginated snippets' + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 2242cb6236a..c30d38b6508 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -82,7 +82,7 @@ feature 'Project', feature: true do before do login_with(user) - project.team.add_user(user, Gitlab::Access::MASTER) + project.add_user(user, Gitlab::Access::MASTER) visit namespace_project_path(project.namespace, project) end @@ -101,8 +101,8 @@ feature 'Project', feature: true do context 'on issues page', js: true do before do login_with(user) - project.team.add_user(user, Gitlab::Access::MASTER) - project2.team.add_user(user, Gitlab::Access::MASTER) + project.add_user(user, Gitlab::Access::MASTER) + project2.add_user(user, Gitlab::Access::MASTER) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index a5ed3595b0a..0e1cc9a0f73 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -60,7 +60,7 @@ describe "Runners" do it "removes specific runner for project if this is last project for that runners" do within ".activated-specific-runners" do - click_on "Remove runner" + click_on "Remove Runner" end expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey @@ -75,7 +75,7 @@ describe "Runners" do end it "enables shared runners" do - click_on "Enable shared runners" + click_on "Enable shared Runners" expect(@project.reload.shared_runners_enabled).to be_truthy end end diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb new file mode 100644 index 00000000000..70b16bfc810 --- /dev/null +++ b/spec/features/snippets_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe 'Snippets', feature: true do + context 'when the project has snippets' do + let(:project) { create(:empty_project, :public) } + let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } + before do + allow(Snippet).to receive(:default_per_page).and_return(1) + visit snippets_path(username: project.owner.username) + end + + it_behaves_like 'paginated snippets' + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index fc555a74f30..bf93c1d1251 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } - let(:issue) { create(:issue) } + let(:issue) { create(:issue, due_date: Date.today) } describe 'GET /dashboard/todos' do context 'User does not have todos' do @@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.todos-list .todo', count: 1) end + it 'shows due date as today' do + page.within first('.todo') do + expect(page).to have_content 'Due today' + end + end + describe 'deleting the todo' do before do first('.done-todo').click diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb index cc40671787c..33b52d1547e 100644 --- a/spec/features/unsubscribe_links_spec.rb +++ b/spec/features/unsubscribe_links_spec.rb @@ -11,7 +11,7 @@ describe 'Unsubscribe links', feature: true do let(:mail) { ActionMailer::Base.deliveries.last } let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) } - let(:header_link) { mail.header['List-Unsubscribe'] } + let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets let(:body_link) { body.find_link('unsubscribe')['href'] } before do diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb index f00abd82fea..ce7e809ec76 100644 --- a/spec/features/users/snippets_spec.rb +++ b/spec/features/users/snippets_spec.rb @@ -3,30 +3,16 @@ require 'spec_helper' describe 'Snippets tab on a user profile', feature: true, js: true do include WaitForAjax - let(:user) { create(:user) } - context 'when the user has snippets' do + let(:user) { create(:user) } + let!(:snippets) { create_list(:snippet, 2, :public, author: user) } before do - create_list(:snippet, 25, :public, author: user) - + allow(Snippet).to receive(:default_per_page).and_return(1) visit user_path(user) page.within('.user-profile-nav') { click_link 'Snippets' } wait_for_ajax end - it 'is limited to 20 items per page' do - expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20) - end - - context 'clicking on the link to the second page' do - before do - click_link('2') - wait_for_ajax - end - - it 'shows the remaining snippets' do - expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5) - end - end + it_behaves_like 'paginated snippets', remote: true end end diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb new file mode 100644 index 00000000000..8cfea9659cb --- /dev/null +++ b/spec/finders/access_requests_finder_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe AccessRequestsFinder, services: true do + let(:user) { create(:user) } + let(:access_requester) { create(:user) } + let(:project) { create(:project, :public) } + let(:group) { create(:group, :public) } + + before do + project.request_access(access_requester) + group.request_access(access_requester) + end + + shared_examples 'a finder returning access requesters' do |method_name| + it 'returns access requesters' do + access_requesters = described_class.new(source).public_send(method_name, user) + + expect(access_requesters.size).to eq(1) + expect(access_requesters.first).to be_a "#{source.class}Member".constantize + expect(access_requesters.first.user).to eq(access_requester) + end + end + + shared_examples 'a finder returning no results' do |method_name| + it 'raises Gitlab::Access::AccessDeniedError' do + expect(described_class.new(source).public_send(method_name, user)).to be_empty + end + end + + shared_examples 'a finder raising Gitlab::Access::AccessDeniedError' do |method_name| + it 'raises Gitlab::Access::AccessDeniedError' do + expect { described_class.new(source).public_send(method_name, user) }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + + describe '#execute' do + context 'when current user cannot see project access requests' do + it_behaves_like 'a finder returning no results', :execute do + let(:source) { project } + end + + it_behaves_like 'a finder returning no results', :execute do + let(:source) { group } + end + end + + context 'when current user can see access requests' do + before do + project.team << [user, :master] + group.add_owner(user) + end + + it_behaves_like 'a finder returning access requesters', :execute do + let(:source) { project } + end + + it_behaves_like 'a finder returning access requesters', :execute do + let(:source) { group } + end + end + end + + describe '#execute!' do + context 'when current user cannot see access requests' do + it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do + let(:source) { project } + end + + it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do + let(:source) { group } + end + end + + context 'when current user can see access requests' do + before do + project.team << [user, :master] + group.add_owner(user) + end + + it_behaves_like 'a finder returning access requesters', :execute! do + let(:source) { project } + end + + it_behaves_like 'a finder returning access requesters', :execute! do + let(:source) { group } + end + end + end +end diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb index f90a8e007c8..29a47e005a6 100644 --- a/spec/finders/joined_groups_finder_spec.rb +++ b/spec/finders/joined_groups_finder_spec.rb @@ -43,7 +43,7 @@ describe JoinedGroupsFinder do context 'if profile visitor is in one of the private group projects' do before do project = create(:project, :private, group: private_group, name: 'B', path: 'B') - project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER) + project.add_user(profile_visitor, Gitlab::Access::DEVELOPER) end it 'shows group' do diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 7a3a74335e8..13bda5f7c5a 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -38,7 +38,7 @@ describe ProjectsFinder do describe 'with private projects' do before do - private_project.team.add_user(user, Gitlab::Access::MASTER) + private_project.add_user(user, Gitlab::Access::MASTER) end it do diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb index a49cbfd5160..cfe15b9defa 100644 --- a/spec/finders/trending_projects_finder_spec.rb +++ b/spec/finders/trending_projects_finder_spec.rb @@ -1,39 +1,48 @@ require 'spec_helper' describe TrendingProjectsFinder do - let(:user) { build(:user) } + let(:user) { create(:user) } + let(:public_project1) { create(:empty_project, :public) } + let(:public_project2) { create(:empty_project, :public) } + let(:private_project) { create(:empty_project, :private) } + let(:internal_project) { create(:empty_project, :internal) } + + before do + 3.times do + create(:note_on_commit, project: public_project1) + end - describe '#execute' do - describe 'without an explicit start date' do - subject { described_class.new } + 2.times do + create(:note_on_commit, project: public_project2, created_at: 5.weeks.ago) + end - it 'returns the trending projects' do - relation = double(:ar_relation) + create(:note_on_commit, project: private_project) + create(:note_on_commit, project: internal_project) + end - allow(subject).to receive(:projects_for) - .with(user) - .and_return(relation) + describe '#execute', caching: true do + context 'without an explicit time range' do + it 'returns public trending projects' do + projects = described_class.new.execute - allow(relation).to receive(:trending) - .with(an_instance_of(ActiveSupport::TimeWithZone)) + expect(projects).to eq([public_project1]) end end - describe 'with an explicit start date' do - let(:date) { 2.months.ago } + context 'with an explicit time range' do + it 'returns public trending projects' do + projects = described_class.new.execute(2) - subject { described_class.new } + expect(projects).to eq([public_project1, public_project2]) + end + end - it 'returns the trending projects' do - relation = double(:ar_relation) + it 'caches the list of projects' do + projects = described_class.new - allow(subject).to receive(:projects_for) - .with(user) - .and_return(relation) + expect(Project).to receive(:trending).once - allow(relation).to receive(:trending) - .with(date) - end + 2.times { projects.execute } end end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 2dd2eab0524..62cc10f579a 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe IssuablesHelper do +describe IssuablesHelper do let(:label) { build_stubbed(:label) } let(:label2) { build_stubbed(:label) } - context 'label tooltip' do + describe '#issuable_labels_tooltip' do it 'returns label text' do expect(issuable_labels_tooltip([label])).to eq(label.title) end @@ -13,4 +13,105 @@ describe IssuablesHelper do expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") end end + + describe '#issuables_state_counter_text' do + let(:user) { create(:user) } + + describe 'state text' do + before do + allow(helper).to receive(:issuables_count_for_state).and_return(42) + end + + it 'returns "Open" when state is :opened' do + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + end + + it 'returns "Closed" when state is :closed' do + expect(helper.issuables_state_counter_text(:issues, :closed)). + to eq('<span>Closed</span> <span class="badge">42</span>') + end + + it 'returns "Merged" when state is :merged' do + expect(helper.issuables_state_counter_text(:merge_requests, :merged)). + to eq('<span>Merged</span> <span class="badge">42</span>') + end + + it 'returns "All" when state is :all' do + expect(helper.issuables_state_counter_text(:merge_requests, :all)). + to eq('<span>All</span> <span class="badge">42</span>') + end + end + + describe 'counter caching based on issuable type and params', :caching do + let(:params) do + { + scope: 'created-by-me', + state: 'opened', + utf8: '✓', + author_id: '11', + assignee_id: '18', + label_name: ['bug', 'discussion', 'documentation'], + milestone_title: 'v4.0', + sort: 'due_date_asc', + namespace_id: 'gitlab-org', + project_id: 'gitlab-ce', + page: 2 + }.with_indifferent_access + end + + it 'returns the cached value when called for the same issuable type & with the same params' do + expect(helper).to receive(:params).twice.and_return(params) + expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + + expect(helper).not_to receive(:issuables_count_for_state) + + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + end + + it 'does not take some keys into account in the cache key' do + expect(helper).to receive(:params).and_return({ + author_id: '11', + state: 'foo', + sort: 'foo', + utf8: 'foo', + page: 'foo' + }.with_indifferent_access) + expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + + expect(helper).to receive(:params).and_return({ + author_id: '11', + state: 'bar', + sort: 'bar', + utf8: 'bar', + page: 'bar' + }.with_indifferent_access) + expect(helper).not_to receive(:issuables_count_for_state) + + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + end + + it 'does not take params order into account in the cache key' do + expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') + expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + + expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') + expect(helper).not_to receive(:issuables_count_for_state) + + expect(helper.issuables_state_counter_text(:issues, :opened)). + to eq('<span>Open</span> <span class="badge">42</span>') + end + end + end end diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 7998209b7b0..6703d88e357 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -11,7 +11,7 @@ describe MembersHelper do describe '#remove_member_message' do let(:requester) { build(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project, :public) } let(:project_member) { build(:project_member, project: project) } let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } } let(:project_member_request) { project.request_access(requester) } @@ -32,7 +32,7 @@ describe MembersHelper do describe '#remove_member_title' do let(:requester) { build(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project, :public) } let(:project_member) { build(:project_member, project: project) } let(:project_member_request) { project.request_access(requester) } let(:group) { create(:group) } diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 840c7b6d015..1ad6f612210 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -48,9 +48,9 @@ setTimeout(() => { expect($('.dropdown-content a').length).toBe(10); - $('.dropdow-content a').each((i, $link) => { - if (i < 5) { - $link.get(0).click(); + $('.dropdown-content a').each(function (i) { + if (i < saveLabelCount) { + $(this).get(0).click(); } }); @@ -70,9 +70,9 @@ setTimeout(() => { expect($('.dropdown-content a').length).toBe(10); - $('.dropdow-content a').each((i, $link) => { - if (i < 5) { - $link.get(0).click(); + $('.dropdown-content a').each(function (i) { + if (i < saveLabelCount) { + $(this).get(0).click(); } }); @@ -86,4 +86,3 @@ }); }); })(); - diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 00d9fc1302a..4470fbcb099 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -112,7 +112,7 @@ fixture.preload('search_autocomplete.html'); beforeEach(function() { fixture.load('search_autocomplete.html'); - return widget = new SearchAutocomplete; + return widget = new gl.SearchAutocomplete; }); it('should show Dashboard specific dropdown menu', function() { var list; diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index b1370bca833..d265d29ee86 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do context "when no language is specified" do it "highlights as plaintext" do result = filter('<pre><code>def fun end</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>def fun end</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>def fun end</code></pre>') end end context "when a valid language is specified" do it "highlights as that language" do result = filter('<pre><code class="ruby">def fun end</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>') end end context "when an invalid language is specified" do it "highlights as plaintext" do result = filter('<pre><code class="gnuplot">This is a test</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>This is a test</code></pre>') end end @@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do it "highlights as plaintext" do result = filter('<pre><code class="ruby">This is a test</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight" v-pre="true"><code>This is a test</code></pre>') end end end diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb deleted file mode 100644 index 569cbc885c7..00000000000 --- a/spec/lib/banzai/filter/task_list_filter_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::TaskListFilter, lib: true do - include FilterSpecHelper - - it 'does not apply `task-list` class to non-task lists' do - exp = act = %(<ul><li>Item</li></ul>) - expect(filter(act).to_html).to eq exp - end - - it 'applies `task-list` to single-item task lists' do - act = filter('<ul><li>[ ] Task 1</li></ul>') - - expect(act.to_html).to start_with '<ul class="task-list">' - end -end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index fdbdb21eac1..729e77fd43f 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -31,13 +31,16 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end it 'supports a special @all mention' do + project.team << [user, :developer] doc = reference_filter("Hey #{reference}", author: user) + expect(doc.css('a').length).to eq 1 expect(doc.css('a').first.attr('href')) .to eq urls.namespace_project_url(project.namespace, project) end it 'includes a data-author attribute when there is an author' do + project.team << [user, :developer] doc = reference_filter(reference, author: user) expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s) @@ -48,6 +51,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(doc.css('a').first.has_attribute?('data-author')).to eq(false) end + + it 'ignores reference to all when the user is not a project member' do + doc = reference_filter("Hey #{reference}", author: user) + + expect(doc.css('a').length).to eq 0 + end end context 'mentioning a user' do diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 745fbc0df45..c9d64e99f88 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -64,7 +64,7 @@ describe Gitlab::Auth, lib: true do it 'recognizes user lfs tokens' do user = create(:user) ip = 'ip' - token = Gitlab::LfsToken.new(user).generate + 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)) @@ -73,7 +73,7 @@ describe Gitlab::Auth, lib: true do it 'recognizes deploy key lfs tokens' do key = create(:deploy_key) ip = 'ip' - token = Gitlab::LfsToken.new(key).generate + 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)) diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 613c47d55f1..e829b936343 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -66,6 +66,6 @@ describe Gitlab::GithubImport::Client, lib: true do stub_request(:get, /api.github.com/) allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound) - expect { client.issues }.not_to raise_error + expect { client.issues {} }.not_to raise_error end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 553c849c9b4..8854c8431b5 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -57,7 +57,8 @@ describe Gitlab::GithubImport::Importer, lib: true do created_at: created_at, updated_at: updated_at, closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347' + url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347', + labels: [double(name: 'Label #1')], ) end @@ -75,7 +76,8 @@ describe Gitlab::GithubImport::Importer, lib: true do created_at: created_at, updated_at: updated_at, closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348' + url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348', + labels: [double(name: 'Label #2')], ) end @@ -94,7 +96,8 @@ describe Gitlab::GithubImport::Importer, lib: true do updated_at: updated_at, closed_at: nil, merged_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' + url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347', + labels: [double(name: 'Label #3')], ) end @@ -129,6 +132,8 @@ describe Gitlab::GithubImport::Importer, lib: true do 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) @@ -148,9 +153,7 @@ describe Gitlab::GithubImport::Importer, lib: true do 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: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, - { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" }, { 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" } diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index ab06b7bc5bb..a73b1f4ff5d 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil) end - context 'when Github project is private' do + context 'when GitHub project is private' do it 'sets project visibility to private' do repo.private = true @@ -43,7 +43,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do end end - context 'when Github project is public' do + context 'when GitHub project is public' do before do allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) end @@ -56,5 +56,25 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) end end + + context 'when GitHub project has wiki' do + it 'does not create the wiki repository' do + allow(repo).to receive(:has_wiki?).and_return(true) + + project = service.execute + + expect(project.wiki.repository_exists?).to eq false + end + end + + context 'when GitHub project does not have wiki' do + it 'creates the wiki repository' do + allow(repo).to receive(:has_wiki?).and_return(false) + + project = service.execute + + expect(project.wiki.repository_exists?).to eq true + end + end end end diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb new file mode 100644 index 00000000000..47d6f1007d1 --- /dev/null +++ b/spec/lib/gitlab/identifier_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe Gitlab::Identifier do + let(:identifier) do + Class.new { include Gitlab::Identifier }.new + end + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:key) { create(:key, user: user) } + + describe '#identify' do + context 'without an identifier' do + it 'identifies the user using a commit' do + expect(identifier).to receive(:identify_using_commit). + with(project, '123') + + identifier.identify('', project, '123') + end + end + + context 'with a user identifier' do + it 'identifies the user using a user ID' do + expect(identifier).to receive(:identify_using_user). + with("user-#{user.id}") + + identifier.identify("user-#{user.id}", project, '123') + end + end + + context 'with an SSH key identifier' do + it 'identifies the user using an SSH key ID' do + expect(identifier).to receive(:identify_using_ssh_key). + with("key-#{key.id}") + + identifier.identify("key-#{key.id}", project, '123') + end + end + end + + describe '#identify_using_commit' do + it "returns the User for an existing commit author's Email address" do + commit = double(:commit, author_email: user.email) + + expect(project).to receive(:commit).with('123').and_return(commit) + + expect(identifier.identify_using_commit(project, '123')).to eq(user) + end + + it 'returns nil when no user could be found' do + allow(project).to receive(:commit).with('123').and_return(nil) + + expect(identifier.identify_using_commit(project, '123')).to be_nil + end + + it 'returns nil when the commit does not have an author Email' do + commit = double(:commit, author_email: nil) + + expect(project).to receive(:commit).with('123').and_return(commit) + + expect(identifier.identify_using_commit(project, '123')).to be_nil + end + + it 'caches the found users per Email' do + commit = double(:commit, author_email: user.email) + + expect(project).to receive(:commit).with('123').twice.and_return(commit) + expect(User).to receive(:find_by).once.and_call_original + + 2.times do + expect(identifier.identify_using_commit(project, '123')).to eq(user) + end + end + end + + describe '#identify_using_user' do + it 'returns the User for an existing ID in the identifier' do + found = identifier.identify_using_user("user-#{user.id}") + + expect(found).to eq(user) + end + + it 'returns nil for a non existing user ID' do + found = identifier.identify_using_user('user--1') + + expect(found).to be_nil + end + + it 'caches the found users per ID' do + expect(User).to receive(:find_by).once.and_call_original + + 2.times do + found = identifier.identify_using_user("user-#{user.id}") + + expect(found).to eq(user) + end + end + end + + describe '#identify_using_ssh_key' do + it 'returns the User for an existing SSH key' do + found = identifier.identify_using_ssh_key("key-#{key.id}") + + expect(found).to eq(user) + end + + it 'returns nil for an invalid SSH key' do + found = identifier.identify_using_ssh_key('key--1') + + expect(found).to be_nil + end + + it 'caches the found users per key' do + expect(User).to receive(:find_by_ssh_key_id).once.and_call_original + + 2.times do + found = identifier.identify_using_ssh_key("key-#{key.id}") + + expect(found).to eq(user) + end + end + end +end diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb index 2ba344092ce..2e19d590d83 100644 --- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb @@ -27,7 +27,7 @@ describe 'Import/Export attribute configuration', lib: true do relation_names.each do |relation_name| relation_class = relation_class_for_name(relation_name) - expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes" + expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes" current_attributes = parsed_attributes(relation_name, relation_class.attribute_names) safe_attributes = safe_model_attributes[relation_class.to_s] diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 056eaa2d719..98323fe6be4 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2232,6 +2232,31 @@ ], "milestones": [ { + "id": 1, + "title": "test milestone", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "events": [ + { + "id": 487, + "target_type": "Milestone", + "target_id": 1, + "title": null, + "data": null, + "project_id": 46, + "created_at": "2016-06-14T15:02:04.418Z", + "updated_at": "2016-06-14T15:02:04.418Z", + "action": 1, + "author_id": 18 + } + ] + }, + { "id": 20, "title": "v4.0", "project_id": 5, @@ -7373,5 +7398,16 @@ } ] } - ] + ], + "project_feature": { + "builds_access_level": 0, + "created_at": "2014-12-26T09:26:45.000Z", + "id": 2, + "issues_access_level": 0, + "merge_requests_access_level": 20, + "project_id": 4, + "snippets_access_level": 20, + "updated_at": "2016-09-23T11:58:28.000Z", + "wiki_access_level": 20 + } }
\ No newline at end of file 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 65d0aaf53d6..7582a732cdf 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -107,6 +107,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(Label.first.label_links.first.target).not_to be_nil end + it 'has a project feature' do + restored_project_json + + expect(project.project_feature).not_to be_nil + end + it 'restores the correct service' do restored_project_json diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 0600893f4cf..563c074017a 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -73,17 +73,33 @@ describe Gitlab::LDAP::Adapter, lib: true do describe '#dn_matches_filter?' do subject { adapter.dn_matches_filter?(:dn, :filter) } + context "when the search result is non-empty" do + before { allow(adapter).to receive(:ldap_search).and_return([:foo]) } + + it { is_expected.to be_truthy } + end + + context "when the search result is empty" do + before { allow(adapter).to receive(:ldap_search).and_return([]) } + + it { is_expected.to be_falsey } + end + end + + describe '#ldap_search' do + subject { adapter.ldap_search(base: :dn, filter: :filter) } + context "when the search is successful" do context "and the result is non-empty" do before { allow(ldap).to receive(:search).and_return([:foo]) } - it { is_expected.to be_truthy } + it { is_expected.to eq [:foo] } end context "and the result is empty" do before { allow(ldap).to receive(:search).and_return([]) } - it { is_expected.to be_falsey } + it { is_expected.to eq [] } end end @@ -95,7 +111,22 @@ describe Gitlab::LDAP::Adapter, lib: true do ) end - it { is_expected.to be_falsey } + it { is_expected.to eq [] } + end + + context "when the search raises an LDAP exception" do + before do + allow(ldap).to receive(:search) { raise Net::LDAP::Error, "some error" } + allow(Rails.logger).to receive(:warn) + end + + it { is_expected.to eq [] } + + it 'logs the error' do + subject + expect(Rails.logger).to have_received(:warn).with( + "LDAP search raised exception Net::LDAP::Error: some error") + end end end end diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 9f04f67e0a8..e9c1163e22a 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Gitlab::LfsToken, lib: true do - describe '#generate and #value' do + describe '#token' do shared_examples 'an LFS token generator' do it 'returns a randomly generated token' do - token = handler.generate + token = handler.token expect(token).not_to be_nil expect(token).to be_a String @@ -12,9 +12,9 @@ describe Gitlab::LfsToken, lib: true do end it 'returns the correct token based on the key' do - token = handler.generate + token = handler.token - expect(handler.value).to eq(token) + expect(handler.token).to eq(token) end end diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index f770857e958..d2d334e6413 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' } before do - project.team.add_user(user, Gitlab::Access::MASTER) + project.add_user(user, Gitlab::Access::MASTER) project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) @@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do context 'when repo is bare or empty' do let(:empty_project) { create(:empty_project) } - before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } + before { empty_project.add_user(user, Gitlab::Access::MASTER) } it "returns empty array" do templates = subject.by_category('', empty_project) @@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do context "when repo is empty" do let(:empty_project) { create(:empty_project) } - before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } + before { empty_project.add_user(user, Gitlab::Access::MASTER) } it "raises file not found" do issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project) diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index bb0f68043fa..ddf68c4cf78 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' } before do - project.team.add_user(user, Gitlab::Access::MASTER) + project.add_user(user, Gitlab::Access::MASTER) project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) @@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do context 'when repo is bare or empty' do let(:empty_project) { create(:empty_project) } - before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } + before { empty_project.add_user(user, Gitlab::Access::MASTER) } it "returns empty array" do templates = subject.by_category('', empty_project) @@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do context "when repo is empty" do let(:empty_project) { create(:empty_project) } - before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } + before { empty_project.add_user(user, Gitlab::Access::MASTER) } it "raises file not found" do issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 0363bc74939..0e4130e8a3a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -402,7 +402,7 @@ describe Notify do describe 'project access requested' do context 'for a project in a user namespace' do - let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } } + let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } } let(:user) { create(:user) } let(:project_member) do project.request_access(user) @@ -429,7 +429,7 @@ describe Notify do context 'for a project in a group' do let(:group_owner) { create(:user) } let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } - let(:project) { create(:project, namespace: group) } + let(:project) { create(:project, :public, namespace: group) } let(:user) { create(:user) } let(:project_member) do project.request_access(user) @@ -492,21 +492,22 @@ describe Notify do end end - def invite_to_project(project:, email:, inviter:) - Member.add_user( - project.project_members, - 'toto@example.com', - Gitlab::Access::DEVELOPER, - current_user: inviter + def invite_to_project(project, inviter:) + create( + :project_member, + :developer, + project: project, + invite_token: '1234', + invite_email: 'toto@example.com', + user: nil, + created_by: inviter ) - - project.project_members.invite.last end describe 'project invitation' do let(:project) { create(:project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } - let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) } + let(:project_member) { invite_to_project(project, inviter: master) } subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) } @@ -525,10 +526,10 @@ describe Notify do describe 'project invitation accepted' do let(:project) { create(:project) } - let(:invited_user) { create(:user) } + let(:invited_user) { create(:user, name: 'invited user') } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:project_member) do - invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master) + invitee = invite_to_project(project, inviter: master) invitee.accept_invite!(invited_user) invitee end @@ -552,7 +553,7 @@ describe Notify do let(:project) { create(:project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:project_member) do - invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master) + invitee = invite_to_project(project, inviter: master) invitee.decline_invite! invitee end @@ -744,21 +745,22 @@ describe Notify do end end - def invite_to_group(group:, email:, inviter:) - Member.add_user( - group.group_members, - 'toto@example.com', - Gitlab::Access::DEVELOPER, - current_user: inviter + def invite_to_group(group, inviter:) + create( + :group_member, + :developer, + group: group, + invite_token: '1234', + invite_email: 'toto@example.com', + user: nil, + created_by: inviter ) - - group.group_members.invite.last end describe 'group invitation' do let(:group) { create(:group) } let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } - let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) } + let(:group_member) { invite_to_group(group, inviter: owner) } subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) } @@ -777,10 +779,10 @@ describe Notify do describe 'group invitation accepted' do let(:group) { create(:group) } - let(:invited_user) { create(:user) } + let(:invited_user) { create(:user, name: 'invited user') } let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } let(:group_member) do - invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner) + invitee = invite_to_group(group, inviter: owner) invitee.accept_invite!(invited_user) invitee end @@ -804,7 +806,7 @@ describe Notify do let(:group) { create(:group) } let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } let(:group_member) do - invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner) + invitee = invite_to_group(group, inviter: owner) invitee.decline_invite! invitee end @@ -829,6 +831,7 @@ describe Notify do let(:user) { create(:user, email: 'old-email@mail.com') } before do + stub_config_setting(email_subject_suffix: 'A Nice Suffix') perform_enqueued_jobs do user.email = "new-email@mail.com" user.save @@ -845,7 +848,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject "Confirmation instructions" + is_expected.to have_subject /^Confirmation instructions/ end it 'includes a link to the site' do diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 56872da9a8f..3956d05060b 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -37,6 +37,16 @@ shared_examples 'an email sent from GitLab' do reply_to = subject.header[:reply_to].addresses expect(reply_to).to eq([gitlab_sender_reply_to]) end + + context 'when custom suffix for email subject is set' do + before do + stub_config_setting(email_subject_suffix: 'A Nice Suffix') + end + + it 'ends the subject with the suffix' do + is_expected.to have_subject /\ \| A Nice Suffix$/ + end + end end shared_examples 'an email that contains a header with author username' do @@ -169,8 +179,9 @@ shared_examples 'it should show Gmail Actions View Commit link' do end shared_examples 'an unsubscribeable thread' do - it 'has a List-Unsubscribe header' do + it 'has a List-Unsubscribe header in the correct format' do is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ + is_expected.to have_header 'List-Unsubscribe', /^<.+>$/ end it { is_expected.to have_body_text /unsubscribe/ } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index e7864b7ad33..ae185de9ca3 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -39,8 +39,8 @@ describe Ci::Build, models: true do end end - describe '#ignored?' do - subject { build.ignored? } + describe '#failed_but_allowed?' do + subject { build.failed_but_allowed? } context 'when build is not allowed to fail' do before do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 2f1baff5d66..80c2a1bc7a9 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -7,7 +7,11 @@ describe CommitStatus, models: true do create(:ci_pipeline, project: project, sha: project.commit.id) end - let(:commit_status) { create(:commit_status, pipeline: pipeline) } + let(:commit_status) { create_status } + + def create_status(args = {}) + create(:commit_status, args.merge(pipeline: pipeline)) + end it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:user) } @@ -125,32 +129,53 @@ describe CommitStatus, models: true do describe '.latest' do subject { CommitStatus.latest.order(:id) } - before do - @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success' - @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success' - @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success' + let(:statuses) do + [create_status(name: 'aa', ref: 'bb', status: 'running'), + create_status(name: 'cc', ref: 'cc', status: 'pending'), + create_status(name: 'aa', ref: 'cc', status: 'success'), + create_status(name: 'cc', ref: 'bb', status: 'success'), + create_status(name: 'aa', ref: 'bb', status: 'success')] end it 'returns unique statuses' do - is_expected.to eq([@commit4, @commit5]) + is_expected.to eq(statuses.values_at(3, 4)) end end describe '.running_or_pending' do subject { CommitStatus.running_or_pending.order(:id) } - before do - @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success' - @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed' - @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled' + let(:statuses) do + [create_status(name: 'aa', ref: 'bb', status: 'running'), + create_status(name: 'cc', ref: 'cc', status: 'pending'), + create_status(name: 'aa', ref: nil, status: 'success'), + create_status(name: 'dd', ref: nil, status: 'failed'), + create_status(name: 'ee', ref: nil, status: 'canceled')] end it 'returns statuses that are running or pending' do - is_expected.to eq([@commit1, @commit2]) + is_expected.to eq(statuses.values_at(0, 1)) + end + end + + describe '.exclude_ignored' do + subject { CommitStatus.exclude_ignored.order(:id) } + + let(:statuses) do + [create_status(when: 'manual', status: 'skipped'), + create_status(when: 'manual', status: 'success'), + create_status(when: 'manual', status: 'failed'), + create_status(when: 'on_failure', status: 'skipped'), + create_status(when: 'on_failure', status: 'success'), + create_status(when: 'on_failure', status: 'failed'), + create_status(allow_failure: true, status: 'success'), + create_status(allow_failure: true, status: 'failed'), + create_status(allow_failure: false, status: 'success'), + create_status(allow_failure: false, status: 'failed')] + end + + it 'returns statuses without what we want to ignore' do + is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9)) end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index e118432d098..87bffbdc54e 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -1,26 +1,17 @@ require 'spec_helper' describe HasStatus do - before do - @object = Object.new - @object.extend(HasStatus::ClassMethods) - end - describe '.status' do - before do - allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses)) - end - - subject { @object.status } + subject { CommitStatus.status } shared_examples 'build status summary' do context 'all successful' do - let(:statuses) { Array.new(2) { create(type, status: :success) } } + let!(:statuses) { Array.new(2) { create(type, status: :success) } } it { is_expected.to eq 'success' } end context 'at least one failed' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :failed)] end @@ -28,7 +19,7 @@ describe HasStatus do end context 'at least one running' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :running)] end @@ -36,7 +27,7 @@ describe HasStatus do end context 'at least one pending' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :pending)] end @@ -44,7 +35,7 @@ describe HasStatus do end context 'success and failed but allowed to fail' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :failed, allow_failure: true)] end @@ -53,12 +44,15 @@ describe HasStatus do end context 'one failed but allowed to fail' do - let(:statuses) { [create(type, status: :failed, allow_failure: true)] } + let!(:statuses) do + [create(type, status: :failed, allow_failure: true)] + end + it { is_expected.to eq 'success' } end context 'success and canceled' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :canceled)] end @@ -66,7 +60,7 @@ describe HasStatus do end context 'one failed and one canceled' do - let(:statuses) do + let!(:statuses) do [create(type, status: :failed), create(type, status: :canceled)] end @@ -74,7 +68,7 @@ describe HasStatus do end context 'one failed but allowed to fail and one canceled' do - let(:statuses) do + let!(:statuses) do [create(type, status: :failed, allow_failure: true), create(type, status: :canceled)] end @@ -83,7 +77,7 @@ describe HasStatus do end context 'one running one canceled' do - let(:statuses) do + let!(:statuses) do [create(type, status: :running), create(type, status: :canceled)] end @@ -91,14 +85,15 @@ describe HasStatus do end context 'all canceled' do - let(:statuses) do + let!(:statuses) do [create(type, status: :canceled), create(type, status: :canceled)] end + it { is_expected.to eq 'canceled' } end context 'success and canceled but allowed to fail' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :canceled, allow_failure: true)] end @@ -107,7 +102,7 @@ describe HasStatus do end context 'one finished and second running but allowed to fail' do - let(:statuses) do + let!(:statuses) do [create(type, status: :success), create(type, status: :running, allow_failure: true)] end @@ -118,11 +113,13 @@ describe HasStatus do context 'ci build statuses' do let(:type) { :ci_build } + it_behaves_like 'build status summary' end context 'generic commit statuses' do let(:type) { :generic_commit_status } + it_behaves_like 'build status summary' end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 549b0042038..132858950d5 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -1,18 +1,27 @@ require 'spec_helper' describe Mentionable do - include Mentionable + class Example + include Mentionable - def author - nil + attr_accessor :project, :message + attr_mentionable :message + + def author + nil + end end describe 'references' do let(:project) { create(:project) } + let(:mentionable) { Example.new } it 'excludes JIRA references' do allow(project).to receive_messages(jira_tracker?: true) - expect(referenced_mentionables(project, 'JIRA-123')).to be_empty + + mentionable.project = project + mentionable.message = 'JIRA-123' + expect(mentionable.referenced_mentionables).to be_empty end end end @@ -39,9 +48,8 @@ describe Issue, "Mentionable" do let(:user) { create(:user) } def referenced_issues(current_user) - text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}" - - issue.referenced_mentionables(current_user, text) + issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}" + issue.referenced_mentionables(current_user) end context 'when the current user can see the issue' do diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb index 743bc2da33f..9d67bc82cba 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -34,6 +34,12 @@ describe CycleAnalytics::Summary, models: true do expect(subject.commits).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) + end end describe "#deploys" do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 8600eb4d2c4..af5002487cc 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -173,13 +173,11 @@ describe Event, models: true do it 'updates the project' do project.update(last_activity_at: 1.year.ago) - expect_any_instance_of(Gitlab::ExclusiveLease). - to receive(:try_obtain).and_return(true) + create_event(project, project.owner) - expect(project).to receive(:update_column). - with(:last_activity_at, a_kind_of(Time)) + project.reload - create_event(project, project.owner) + project.last_activity_at <= 1.minute.ago end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 3259f795296..3b8b743af2d 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -494,7 +494,7 @@ describe Issue, models: true do context 'with an admin user' do let(:project) { create(:empty_project) } - let(:user) { create(:user, admin: true) } + let(:user) { create(:admin) } it 'returns true for a regular issue' do issue = build(:issue, project: project) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 0b1634f654a..485121701af 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -57,7 +57,7 @@ describe Member, models: true do describe 'Scopes & finders' do before do - project = create(:empty_project) + project = create(:empty_project, :public) group = create(:group) @owner_user = create(:user).tap { |u| group.add_owner(u) } @owner = group.members.find_by(user_id: @owner_user.id) @@ -74,22 +74,17 @@ describe Member, models: true do @blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER) @blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER) - Member.add_user( - project.members, - 'toto1@example.com', - Gitlab::Access::DEVELOPER, - current_user: @master_user - ) - @invited_member = project.members.invite.find_by_invite_email('toto1@example.com') + @invited_member = create(:project_member, :developer, + project: project, + invite_token: '1234', + invite_email: 'toto1@example.com') accepted_invite_user = build(:user, state: :active) - Member.add_user( - project.members, - 'toto2@example.com', - Gitlab::Access::DEVELOPER, - current_user: @master_user - ) - @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) } + @accepted_invite_member = create(:project_member, :developer, + project: project, + invite_token: '1234', + invite_email: 'toto2@example.com'). + tap { |u| u.accept_invite!(accepted_invite_user) } requested_user = create(:user).tap { |u| project.request_access(u) } @requested_member = project.requesters.find_by(user_id: requested_user.id) @@ -176,39 +171,209 @@ describe Member, models: true do it { is_expected.to respond_to(:user_email) } end - describe ".add_user" do - let!(:user) { create(:user) } - let(:project) { create(:project) } + describe '.add_user' do + %w[project group].each do |source_type| + context "when source is a #{source_type}" do + let!(:source) { create(source_type, :public) } + let!(:user) { create(:user) } + let!(:admin) { create(:admin) } - context "when called with a user id" do - it "adds the user as a member" do - Member.add_user(project.project_members, user.id, ProjectMember::MASTER) + it 'returns a <Source>Member object' do + member = described_class.add_user(source, user, :master) - expect(project.users).to include(user) - end - end + expect(member).to be_a "#{source_type.classify}Member".constantize + expect(member).to be_persisted + end - context "when called with a user object" do - it "adds the user as a member" do - Member.add_user(project.project_members, user, ProjectMember::MASTER) + it 'sets members.created_by to the given current_user' do + member = described_class.add_user(source, user, :master, current_user: admin) - expect(project.users).to include(user) - end - end + expect(member.created_by).to eq(admin) + end - context "when called with a known user email" do - it "adds the user as a member" do - Member.add_user(project.project_members, user.email, ProjectMember::MASTER) + it 'sets members.expires_at to the given expires_at' do + member = described_class.add_user(source, user, :master, expires_at: Date.new(2016, 9, 22)) - expect(project.users).to include(user) - end - end + expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + end + + described_class.access_levels.each do |sym_key, int_access_level| + it "accepts the :#{sym_key} symbol as access level" do + expect(source.users).not_to include(user) + + member = described_class.add_user(source, user.id, sym_key) + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + + it "accepts the #{int_access_level} integer as access level" do + expect(source.users).not_to include(user) + + member = described_class.add_user(source, user.id, int_access_level) + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + end + + context 'with no current_user' do + context 'when called with a known user id' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.add_user(source, user.id, :master) + + expect(source.users.reload).to include(user) + end + end + + context 'when called with an unknown user id' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.add_user(source, 42, :master) + + expect(source.users.reload).not_to include(user) + end + end + + context 'when called with a user object' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.add_user(source, user, :master) + + expect(source.users.reload).to include(user) + end + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member' do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + expect { described_class.add_user(source, user, :master) }. + to raise_error(Gitlab::Access::AccessDeniedError) + + expect(source.users.reload).not_to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_truthy + end + end + + context 'when called with a known user email' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.add_user(source, user.email, :master) + + expect(source.users.reload).to include(user) + end + end + + context 'when called with an unknown user email' do + it 'creates an invited member' do + expect(source.users).not_to include(user) + + described_class.add_user(source, 'user@example.com', :master) + + expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') + end + end + end + + context 'when current_user can update member' do + it 'creates the member' do + expect(source.users).not_to include(user) + + described_class.add_user(source, user, :master, current_user: admin) + + expect(source.users.reload).to include(user) + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member' do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + described_class.add_user(source, user, :master, current_user: admin) + + expect(source.users.reload).to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_falsy + end + end + end + + context 'when current_user cannot update member' do + it 'does not create the member' do + expect(source.users).not_to include(user) + + member = described_class.add_user(source, user, :master, current_user: user) + + expect(source.users.reload).not_to include(user) + expect(member).not_to be_persisted + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'does not destroy the requester' do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + described_class.add_user(source, user, :master, current_user: user) + + expect(source.users.reload).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + end + end + end + + context 'when member already exists' do + before do + source.add_user(user, :developer) + end + + context 'with no current_user' do + it 'updates the member' do + expect(source.users).to include(user) + + described_class.add_user(source, user, :master) + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER) + end + end + + context 'when current_user can update member' do + it 'updates the member' do + expect(source.users).to include(user) + + described_class.add_user(source, user, :master, current_user: admin) + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER) + end + end + + context 'when current_user cannot update member' do + it 'does not update the member' do + expect(source.users).to include(user) - context "when called with an unknown user email" do - it "adds a member invite" do - Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER) + described_class.add_user(source, user, :master, current_user: user) - expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com") + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + end end end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 56fa7fa6134..370aeb9e0a9 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -1,6 +1,33 @@ require 'spec_helper' describe GroupMember, models: true do + describe '.access_level_roles' do + it 'returns Gitlab::Access.options_with_owner' do + expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner) + end + end + + describe '.access_levels' do + it 'returns Gitlab::Access.options_with_owner' do + expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner) + end + end + + describe '.add_users_to_group' do + it 'adds the given users to the given group' do + group = create(:group) + users = create_list(:user, 2) + + described_class.add_users_to_group( + group, + [users.first.id, users.second], + described_class::MASTER + ) + + expect(group.users).to include(users.first, users.second) + end + end + describe 'notifications' do describe "#after_create" do it "sends email to user" do diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 805c15a4e5e..d85a1c1e3b2 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -15,6 +15,26 @@ describe ProjectMember, models: true do it { is_expected.to include_module(Gitlab::ShellAdapter) } end + describe '.access_level_roles' do + it 'returns Gitlab::Access.options' do + expect(described_class.access_level_roles).to eq(Gitlab::Access.options) + end + end + + describe '.add_user' do + context 'when called with the project owner' do + it 'adds the user as a member' do + project = create(:empty_project) + + expect(project.users).not_to include(project.owner) + + described_class.add_user(project, project.owner, :master, current_user: project.owner) + + expect(project.users.reload).to include(project.owner) + end + end + end + describe '#real_source_type' do subject { create(:project_member).real_source_type } @@ -50,7 +70,7 @@ describe ProjectMember, models: true do end end - describe :import_team do + describe '.import_team' do before do @project_1 = create :project @project_2 = create :project @@ -81,25 +101,21 @@ describe ProjectMember, models: true do end describe '.add_users_to_projects' do - before do - @project_1 = create :project - @project_2 = create :project + it 'adds the given users to the given projects' do + projects = create_list(:empty_project, 2) + users = create_list(:user, 2) - @user_1 = create :user - @user_2 = create :user - - ProjectMember.add_users_to_projects( - [@project_1.id, @project_2.id], - [@user_1.id, @user_2.id], - ProjectMember::MASTER - ) - end + described_class.add_users_to_projects( + [projects.first.id, projects.second], + [users.first.id, users.second], + described_class::MASTER) - it { expect(@project_1.users).to include(@user_1) } - it { expect(@project_1.users).to include(@user_2) } + expect(projects.first.users).to include(users.first) + expect(projects.first.users).to include(users.second) - it { expect(@project_2.users).to include(@user_1) } - it { expect(@project_2.users).to include(@user_2) } + expect(projects.second.users).to include(users.first) + expect(projects.second.users).to include(users.second) + end end describe '.truncate_teams' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 12df6adde44..38b6da50168 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -86,6 +86,30 @@ describe MergeRequest, models: true do end end + describe '#cache_merge_request_closes_issues!' do + before do + subject.project.team << [subject.author, :developer] + subject.target_branch = subject.project.default_branch + end + + it 'caches closed issues' do + issue = create :issue, project: subject.project + commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) + + expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1) + end + + it 'does not cache issues from external trackers' do + subject.project.update_attribute(:has_external_issue_tracker, true) + issue = ExternalIssue.new('JIRA-123', subject.project) + commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) + + expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count) + end + end + describe '#source_branch_sha' do let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } @@ -287,6 +311,46 @@ describe MergeRequest, models: true do end end + describe "#wipless_title" do + ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix| + it "removes the '#{wip_prefix}' prefix" do + wipless_title = subject.title + subject.title = "#{wip_prefix}#{subject.title}" + + expect(subject.wipless_title).to eq wipless_title + end + + it "is satisfies the #work_in_progress? method" do + subject.title = "#{wip_prefix}#{subject.title}" + subject.title = subject.wipless_title + + expect(subject.work_in_progress?).to eq false + end + end + end + + describe "#wip_title" do + it "adds the WIP: prefix to the title" do + wip_title = "WIP: #{subject.title}" + + expect(subject.wip_title).to eq wip_title + end + + it "does not add the WIP: prefix multiple times" do + wip_title = "WIP: #{subject.title}" + subject.title = subject.wip_title + subject.title = subject.wip_title + + expect(subject.wip_title).to eq wip_title + end + + it "is satisfies the #work_in_progress? method" do + subject.title = subject.wip_title + + expect(subject.work_in_progress?).to eq true + end + end + describe '#can_remove_source_branch?' do let(:user) { create(:user) } let(:user2) { create(:user) } @@ -328,6 +392,42 @@ describe MergeRequest, models: true do end end + describe '#merge_commit_message' do + it 'includes merge information as the title' do + request = build(:merge_request, source_branch: 'source', target_branch: 'target') + + expect(request.merge_commit_message) + .to match("Merge branch 'source' into 'target'\n\n") + end + + it 'includes its title in the body' do + request = build(:merge_request, title: 'Remove all technical debt') + + expect(request.merge_commit_message) + .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') + + expect(request.merge_commit_message) + .to match("By removing all code\n\n") + end + + it 'includes its reference in the body' do + request = build_stubbed(:merge_request) + + expect(request.merge_commit_message) + .to match("See merge request #{request.to_reference}") + end + + it 'excludes multiple linebreak runs when description is blank' do + request = build(:merge_request, title: 'Title', description: nil) + + expect(request.merge_commit_message).not_to match("Title\n\n\n\n") + end + end + describe "#reset_merge_when_build_succeeds" do let(:merge_if_green) do create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user), @@ -446,7 +546,7 @@ describe MergeRequest, models: true do end it_behaves_like 'an editable mentionable' do - subject { create(:merge_request) } + subject { create(:merge_request, :simple) } let(:backref_text) { "merge request #{subject.to_reference}" } let(:set_mentionable_text) { ->(txt){ subject.description = txt } } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index d64d6cde2b5..33fe22dd98c 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -20,10 +20,10 @@ describe Milestone, models: true do let(:user) { create(:user) } describe "#title" do - let(:milestone) { create(:milestone, title: "<b>test</b>") } + let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") } it "sanitizes title" do - expect(milestone.title).to eq("test") + expect(milestone.title).to eq("foo & bar -> 2.2") end end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index 0f8889bdf3c..98c36ec088d 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -7,7 +7,7 @@ describe SlackService::IssueMessage, models: true do { user: { name: 'Test User', - username: 'Test User' + username: 'test.user' }, project_name: 'project_name', project_url: 'somewhere.com', @@ -40,7 +40,7 @@ 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') + '<somewhere.com|[project_name>] Issue opened by test.user') expect(subject.attachments).to eq([ { title: "#100 Issue title", @@ -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') + '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb index 224c7ceabe8..c5c052d9af1 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -7,7 +7,7 @@ describe SlackService::MergeMessage, models: true do { user: { name: 'Test User', - username: 'Test User' + username: 'test.user' }, project_name: 'project_name', project_url: 'somewhere.com', @@ -31,7 +31,7 @@ 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> '\ + 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\ 'in <somewhere.com|project_name>: *Issue title*') expect(subject.attachments).to be_empty end @@ -43,7 +43,7 @@ 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> '\ + 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\ 'in <somewhere.com|project_name>: *Issue title*') expect(subject.attachments).to be_empty end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index 41b93f08050..38cfe4ad3e3 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -7,7 +7,7 @@ describe SlackService::NoteMessage, models: true do @args = { user: { name: 'Test User', - username: 'username', + username: 'test.user', avatar_url: 'http://fakeavatar' }, project_name: 'project_name', @@ -37,7 +37,7 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on commits' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("Test User commented on " \ + expect(message.pretext).to eq("test.user commented on " \ "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \ "*Added a commit message*") expected_attachments = [ @@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on a merge request' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("Test User commented on " \ + expect(message.pretext).to eq("test.user commented on " \ "<url|merge request !30> in <somewhere.com|project_name>: " \ "*merge request title*") expected_attachments = [ @@ -90,7 +90,7 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = SlackService::NoteMessage.new(@args) expect(message.pretext).to eq( - "Test User commented on " \ + "test.user commented on " \ "<url|issue #20> in <somewhere.com|project_name>: " \ "*issue title*") expected_attachments = [ @@ -115,7 +115,7 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on a project snippet' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("Test User commented on " \ + expect(message.pretext).to eq("test.user commented on " \ "<url|snippet #5> in <somewhere.com|project_name>: " \ "*snippet title*") expected_attachments = [ diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb index cda9ee670b0..17cd05e24f1 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -9,7 +9,7 @@ describe SlackService::PushMessage, models: true do before: 'before', project_name: 'project_name', ref: 'refs/heads/master', - user_name: 'user_name', + user_name: 'test.user', project_url: 'url' } end @@ -26,7 +26,7 @@ describe SlackService::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'user_name pushed to branch <url/commits/master|master> of '\ + 'test.user pushed to branch <url/commits/master|master> of '\ '<url|project_name> (<url/compare/before...after|Compare changes>)' ) expect(subject.attachments).to eq([ @@ -46,13 +46,13 @@ describe SlackService::PushMessage, models: true do before: Gitlab::Git::BLANK_SHA, project_name: 'project_name', ref: 'refs/tags/new_tag', - user_name: 'user_name', + user_name: 'test.user', project_url: 'url' } end it 'returns a message regarding pushes' do - expect(subject.pretext).to eq('user_name pushed new tag ' \ + expect(subject.pretext).to eq('test.user pushed new tag ' \ '<url/commits/new_tag|new_tag> to ' \ '<url|project_name>') expect(subject.attachments).to be_empty @@ -66,7 +66,7 @@ describe SlackService::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'user_name pushed new branch <url/commits/master|master> to '\ + 'test.user pushed new branch <url/commits/master|master> to '\ '<url|project_name>' ) expect(subject.attachments).to be_empty @@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'user_name removed branch master from <url|project_name>' + 'test.user removed branch master from <url|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/slack_service/wiki_page_message_spec.rb index 13aea0b0600..093911598b0 100644 --- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb +++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb @@ -7,7 +7,7 @@ describe SlackService::WikiPageMessage, models: true do { user: { name: 'Test User', - username: 'Test User' + username: 'test.user' }, project_name: 'project_name', project_url: 'somewhere.com', @@ -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 <url|wiki page> in <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 <url|wiki page> in <somewhere.com|project_name>: '\ '*Wiki page title*') end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 83f61f0af0a..e52d4aaf884 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -68,7 +68,7 @@ describe Project, models: true do it { is_expected.to have_many(:forks).through(:forked_project_links) } describe '#members & #requesters' do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:requester) { create(:user) } let(:developer) { create(:user) } before do @@ -308,8 +308,7 @@ describe Project, models: true do end describe 'last_activity methods' do - let(:timestamp) { Time.now - 2.hours } - let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) } + let(:project) { create(:project, last_activity_at: 2.hours.ago) } describe 'last_activity' do it 'alias last_activity to last_event' do @@ -321,7 +320,6 @@ describe Project, models: true do describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do - expect_any_instance_of(Event).to receive(:try_obtain_lease).and_return(true) new_event = create(:event, project: project, created_at: Time.now) expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i) @@ -826,6 +824,14 @@ describe Project, models: true do expect(subject).to eq([project2, project1]) end end + + it 'does not take system notes into account' do + 10.times do + create(:note_on_commit, project: project2, system: true) + end + + expect(described_class.trending.to_a).to eq([project1, project2]) + end end describe '.visible_to_user' do @@ -836,7 +842,7 @@ describe Project, models: true do describe 'when a user has access to a project' do before do - project.team.add_user(user, Gitlab::Access::MASTER) + project.add_user(user, Gitlab::Access::MASTER) end it { is_expected.to eq([project]) } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index f979d66c88c..e0f2dadf189 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -137,7 +137,7 @@ describe ProjectTeam, models: true do describe '#find_member' do context 'personal project' do - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:requester) { create(:user) } before do @@ -200,7 +200,7 @@ describe ProjectTeam, models: true do let(:requester) { create(:user) } context 'personal project' do - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } context 'when project is not shared with group' do before do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index db29f4d353b..98c64c079b9 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -320,6 +320,16 @@ describe Repository, models: true do end end + describe '#create_ref' do + it 'redirects the call to fetch_ref' do + ref, ref_path = '1', '2' + + expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path) + + repository.create_ref(ref, ref_path) + end + end + describe "#changelog" do before do repository.send(:cache).expire(:changelog) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 05056a4bb47..ed1bc9271ae 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -203,6 +203,23 @@ describe Service, models: true do end end + describe 'initialize service with no properties' do + let(:service) do + GitlabIssueTrackerService.create( + project: create(:project), + title: 'random title' + ) + end + + it 'does not raise error' do + expect { service }.not_to raise_error + end + + it 'creates the properties' do + expect(service.properties).to eq({ "title" => "random title" }) + end + end + describe "callbacks" do let(:project) { create(:project) } let!(:service) do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a1770d96f83..65b2896930a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -610,6 +610,23 @@ describe User, models: true do end end + describe '.find_by_ssh_key_id' do + context 'using an existing SSH key ID' do + let(:user) { create(:user) } + let(:key) { create(:key, user: user) } + + it 'returns the corresponding User' do + expect(described_class.find_by_ssh_key_id(key.id)).to eq(user) + end + end + + context 'using an invalid SSH key ID' do + it 'returns nil' do + expect(described_class.find_by_ssh_key_id(-1)).to be_nil + end + end + end + describe '.by_login' do let(:username) { 'John' } let!(:user) { create(:user, username: username) } diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index d78494b76fa..b467890a403 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -64,12 +64,12 @@ describe API::AccessRequests, api: true do context 'when authenticated as a member' do %i[developer master].each do |type| context "as a #{type}" do - it 'returns 400' do + it 'returns 403' do expect do user = public_send(type) post api("/#{source_type.pluralize}/#{source.id}/access_requests", user) - expect(response).to have_http_status(400) + expect(response).to have_http_status(403) end.not_to change { source.requesters.count } end end @@ -87,6 +87,20 @@ describe API::AccessRequests, api: true do end context 'when authenticated as a stranger' do + context "when access request is disabled for the #{source_type}" do + before do + source.update(request_access_enabled: false) + end + + it 'returns 403' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) + + expect(response).to have_http_status(403) + end.not_to change { source.requesters.count } + end + end + it 'returns 201' do expect do post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) @@ -181,7 +195,7 @@ describe API::AccessRequests, api: true do end context 'when authenticated as the access requester' do - it 'returns 200' do + it 'deletes the access requester' do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) @@ -191,7 +205,7 @@ describe API::AccessRequests, api: true do end context 'when authenticated as a master/owner' do - it 'returns 200' do + it 'deletes the access requester' do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) @@ -199,6 +213,16 @@ describe API::AccessRequests, api: true do end.to change { source.requesters.count }.by(-1) end + context 'user_id matches a member, not an access requester' do + it 'returns 404' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master) + + expect(response).to have_http_status(404) + end.not_to change { source.requesters.count } + end + end + context 'user_id does not match an existing access requester' do it 'returns 404' do expect do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index ee0b61e2ca4..95c7bbf99c9 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -30,6 +30,15 @@ describe API::API, api: true do expect(json_response.first['commit']['id']).to eq project.commit.id end + it 'returns pipeline data' do + json_build = json_response.first + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + context 'filter project with one scope element' do let(:query) { 'scope=pending' } @@ -91,6 +100,15 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.size).to eq 2 end + + it 'returns pipeline data' do + json_build = json_response.first + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end end context 'when pipeline has no builds' do @@ -133,6 +151,15 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['name']).to eq('test') end + + it 'returns pipeline data' do + json_build = json_response + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end end context 'unauthorized user' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 1f68ef1af8f..3ba257256a0 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -45,6 +45,16 @@ describe API::API, api: true do expect(json_response.length).to eq(2) end end + + context "when using skip_groups in request" do + it "returns all groups excluding skipped groups" do + get api("/groups", admin), skip_groups: [group2.id] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + end + end end describe "GET /groups/:id" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 46e8e6f1169..f0f590b0331 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -111,7 +111,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['username']).to eq(user.username) - expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value) + expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) end @@ -131,7 +131,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}") - expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value) + expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a7930c59df9..b813ee967f8 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -15,7 +15,7 @@ describe API::API, api: true do let(:milestone) { create(:milestone, title: '1.0.0', project: project) } before do - project.team << [user, :reporters] + project.team << [user, :reporter] end describe "GET /projects/:id/merge_requests" do @@ -299,7 +299,7 @@ describe API::API, api: true do let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before :each do |each| - fork_project.team << [user2, :reporters] + fork_project.team << [user2, :reporter] end it "returns merge_request" do diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index b89dac01040..dd192bea432 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -104,6 +104,14 @@ describe API::API, api: true do expect(response).to have_http_status(400) end + + it 'creates a new project with reserved html characters' do + post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') + expect(json_response['description']).to be_nil + end end describe 'PUT /projects/:id/milestones/:milestone_id' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 192c7d14c13..4a0d727faea 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -761,13 +761,16 @@ describe API::API, api: true do let(:group) { create(:group) } it "shares project with group" do + expires_at = 10.days.from_now.to_date + expect do - post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at end.to change { ProjectGroupLink.count }.by(1) expect(response.status).to eq 201 - expect(json_response['group_id']).to eq group.id - expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER + expect(json_response['group_id']).to eq(group.id) + expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) + expect(json_response['expires_at']).to eq(expires_at.to_s) end it "returns a 400 error when group id is not given" do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 54d096e8b7f..f4903d8e0be 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -14,22 +14,38 @@ describe API::API, 'Settings', api: true do expect(json_response['default_projects_limit']).to eq(42) expect(json_response['signin_enabled']).to be_truthy expect(json_response['repository_storage']).to eq('default') + expect(json_response['koding_enabled']).to be_falsey + expect(json_response['koding_url']).to be_nil end end describe "PUT /application/settings" do - before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + context "custom repository storage type set in the config" do + before do + storages = { 'custom' => 'tmp/tests/custom_repositories' } + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + end + + 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' + expect(response).to have_http_status(200) + expect(json_response['default_projects_limit']).to eq(3) + expect(json_response['signin_enabled']).to be_falsey + expect(json_response['repository_storage']).to eq('custom') + expect(json_response['koding_enabled']).to be_truthy + expect(json_response['koding_url']).to eq('http://koding.example.com') + end end - it "updates application settings" do - put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom' - expect(response).to have_http_status(200) - expect(json_response['default_projects_limit']).to eq(3) - expect(json_response['signin_enabled']).to be_falsey - expect(json_response['repository_storage']).to eq('custom') + context "missing koding_url value when koding_enabled is true" do + it "returns a blank parameter error message" 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" + end end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ef73778efa9..f4ea3bebb4c 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -62,6 +62,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first.keys).to include 'email' + expect(json_response.first.keys).to include 'organization' expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'two_factor_enabled' @@ -265,6 +266,14 @@ describe API::API, api: true do expect(user.reload.bio).to eq('new test bio') end + it "updates user with organization" do + put api("/users/#{user.id}", admin), { organization: 'GitLab' } + + expect(response).to have_http_status(200) + expect(json_response['organization']).to eq('GitLab') + expect(user.reload.organization).to eq('GitLab') + end + it 'updates user with his own email' do put api("/users/#{user.id}", admin), email: user.email expect(response).to have_http_status(200) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index df97f1bf7b6..7b7d62feb2c 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -35,18 +35,24 @@ describe Ci::API::API do end end - it "starts a build" do - register_builds info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response['sha']).to eq(build.sha) - expect(runner.reload.platform).to eq("darwin") - expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) - expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true } - ) + context 'when there is a pending build' do + it 'starts a build' do + register_builds info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(build.sha) + expect(runner.reload.platform).to eq("darwin") + expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) + expect(json_response["variables"]).to include( + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "DB_NAME", "value" => "postgres", "public" => true } + ) + end + + it 'updates runner info' do + expect { register_builds }.to change { runner.reload.contacted_at } + end end context 'when builds are finished' do @@ -159,13 +165,18 @@ describe Ci::API::API do end context 'when runner is paused' do - let(:inactive_runner) { create(:ci_runner, :inactive, token: "InactiveRunner") } + let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') } - before do - register_builds inactive_runner.token + it 'responds with 404' do + register_builds + + expect(response).to have_http_status 404 end - it { expect(response).to have_http_status 404 } + it 'does not update runner info' do + expect { register_builds } + .not_to change { runner.reload.contacted_at } + end end def register_builds(token = runner.token, **params) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 74516686921..c0c1e62e910 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,508 +1,517 @@ require "spec_helper" describe 'Git HTTP requests', lib: true do + include GitHttpHelpers include WorkhorseHelpers - let(:user) { create(:user) } - let(:project) { create(:project, path: 'project.git-project') } - it "gives WWW-Authenticate hints" do clone_get('doesnt/exist.git') expect(response.header['WWW-Authenticate']).to start_with('Basic ') end - context "when the project doesn't exist" do - context "when no authentication is provided" do - it "responds with status 401 (no project existence information leak)" do - download('doesnt/exist.git') do |response| - expect(response).to have_http_status(401) - end - end - end + describe "User with no identities" do + let(:user) { create(:user) } + let(:project) { create(:project, path: 'project.git-project') } - context "when username and password are provided" do - context "when authentication fails" do - it "responds with status 401" do - download('doesnt/exist.git', user: user.username, password: "nope") do |response| + context "when the project doesn't exist" do + context "when no authentication is provided" do + it "responds with status 401 (no project existence information leak)" do + download('doesnt/exist.git') do |response| expect(response).to have_http_status(401) end end end - context "when authentication succeeds" do - it "responds with status 404" do - download('/doesnt/exist.git', user: user.username, password: user.password) do |response| - expect(response).to have_http_status(404) + context "when username and password are provided" do + context "when authentication fails" do + it "responds with status 401" do + download('doesnt/exist.git', user: user.username, password: "nope") do |response| + expect(response).to have_http_status(401) + end end end - end - end - end - - context "when the Wiki for a project exists" do - it "responds with the right project" do - wiki = ProjectWiki.new(project) - project.update_attribute(:visibility_level, Project::PUBLIC) - download("/#{wiki.repository.path_with_namespace}.git") do |response| - json_body = ActiveSupport::JSON.decode(response.body) - - expect(response).to have_http_status(200) - expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + context "when authentication succeeds" do + it "responds with status 404" do + download('/doesnt/exist.git', user: user.username, password: user.password) do |response| + expect(response).to have_http_status(404) + end + end + end end end - end - context "when the project exists" do - let(:path) { "#{project.path_with_namespace}.git" } - - context "when the project is public" do - before do + context "when the Wiki for a project exists" do + it "responds with the right project" do + wiki = ProjectWiki.new(project) project.update_attribute(:visibility_level, Project::PUBLIC) - end - it "downloads get status 200" do - download(path, {}) do |response| + download("/#{wiki.repository.path_with_namespace}.git") do |response| + json_body = ActiveSupport::JSON.decode(response.body) + expect(response).to have_http_status(200) + expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end + end + + context "when the project exists" do + let(:path) { "#{project.path_with_namespace}.git" } - it "uploads get status 401" do - upload(path, {}) do |response| - expect(response).to have_http_status(401) + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) end - end - context "with correct credentials" do - let(:env) { { user: user.username, password: user.password } } + it "downloads get status 200" do + download(path, {}) do |response| + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + end - it "uploads get status 403" do - upload(path, env) do |response| - expect(response).to have_http_status(403) + it "uploads get status 401" do + upload(path, {}) do |response| + expect(response).to have_http_status(401) end end - context 'but git-receive-pack is disabled' do - it "responds with status 404" do - allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) + context "with correct credentials" do + let(:env) { { user: user.username, password: user.password } } + it "uploads get status 403" do upload(path, env) do |response| expect(response).to have_http_status(403) end end - end - end - context 'but git-upload-pack is disabled' do - it "responds with status 404" do - allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) + context 'but git-receive-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) - download(path, {}) do |response| - expect(response).to have_http_status(404) + upload(path, env) do |response| + expect(response).to have_http_status(403) + end + end end end - end - - context 'when the request is not from gitlab-workhorse' do - it 'raises an exception' do - expect do - get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack") - end.to raise_error(JWT::DecodeError) - end - end - end - context "when the project is private" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end + context 'but git-upload-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) - context "when no authentication is provided" do - it "responds with status 401 to downloads" do - download(path, {}) do |response| - expect(response).to have_http_status(401) + download(path, {}) do |response| + expect(response).to have_http_status(404) + end end end - it "responds with status 401 to uploads" do - upload(path, {}) do |response| - expect(response).to have_http_status(401) + context 'when the request is not from gitlab-workhorse' do + it 'raises an exception' do + expect do + get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack") + end.to raise_error(JWT::DecodeError) end end end - context "when username and password are provided" do - let(:env) { { user: user.username, password: 'nope' } } + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end - context "when authentication fails" do - it "responds with status 401" do - download(path, env) do |response| + context "when no authentication is provided" do + it "responds with status 401 to downloads" do + download(path, {}) do |response| expect(response).to have_http_status(401) end end - context "when the user is IP banned" do - it "responds with status 401" do - expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) - allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - - clone_get(path, env) - + it "responds with status 401 to uploads" do + upload(path, {}) do |response| expect(response).to have_http_status(401) end end end - context "when authentication succeeds" do - let(:env) { { user: user.username, password: user.password } } + context "when username and password are provided" do + let(:env) { { user: user.username, password: 'nope' } } - context "when the user has access to the project" do - before do - project.team << [user, :master] + context "when authentication fails" do + it "responds with status 401" do + download(path, env) do |response| + expect(response).to have_http_status(401) + end end - context "when the user is blocked" do - it "responds with status 404" do - user.block - project.team << [user, :master] + context "when the user is IP banned" do + it "responds with status 401" do + expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) + allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - download(path, env) do |response| - expect(response).to have_http_status(404) - end + clone_get(path, env) + + expect(response).to have_http_status(401) end end + end - context "when the user isn't blocked" do - it "downloads get status 200" do - expect(Rack::Attack::Allow2Ban).to receive(:reset) + context "when authentication succeeds" do + let(:env) { { user: user.username, password: user.password } } - clone_get(path, env) + context "when the user has access to the project" do + before do + project.team << [user, :master] + end - expect(response).to have_http_status(200) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + context "when the user is blocked" do + it "responds with status 404" do + user.block + project.team << [user, :master] + + download(path, env) do |response| + expect(response).to have_http_status(404) + end + end end - it "uploads get status 200" do - upload(path, env) do |response| + context "when the user isn't blocked" do + it "downloads get status 200" do + expect(Rack::Attack::Allow2Ban).to receive(:reset) + + clone_get(path, env) + expect(response).to have_http_status(200) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end - end - end - context "when an oauth token is provided" do - before do - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + it "uploads get status 200" do + upload(path, env) do |response| + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + end end - it "downloads get status 200" do - clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + context "when an oauth token is provided" do + before do + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + end - expect(response).to have_http_status(200) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - end + it "downloads get status 200" do + clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token - it "uploads get status 401 (no project existence information leak)" do - push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end - expect(response).to have_http_status(401) + it "uploads get status 401 (no project existence information leak)" do + push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + + expect(response).to have_http_status(401) + end end - end - context 'when user has 2FA enabled' do - let(:user) { create(:user, :two_factor) } - let(:access_token) { create(:personal_access_token, user: user) } + context 'when user has 2FA enabled' do + let(:user) { create(:user, :two_factor) } + let(:access_token) { create(:personal_access_token, user: user) } - before do - project.team << [user, :master] - end + before do + project.team << [user, :master] + end - context 'when username and password are provided' do - it 'rejects the clone attempt' do - download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response| - expect(response).to have_http_status(401) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + context 'when username and password are provided' do + it 'rejects the clone attempt' do + download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response| + expect(response).to have_http_status(401) + expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + end end - end - it 'rejects the push attempt' do - upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response| - expect(response).to have_http_status(401) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + it 'rejects the push attempt' do + upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response| + expect(response).to have_http_status(401) + expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + end end end - end - context 'when username and personal access token are provided' do - it 'allows clones' do - download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response| - expect(response).to have_http_status(200) + context 'when username and personal access token are provided' do + it 'allows clones' do + download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response| + expect(response).to have_http_status(200) + end end - end - it 'allows pushes' do - upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response| - expect(response).to have_http_status(200) + it 'allows pushes' do + upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response| + expect(response).to have_http_status(200) + end end end end - end - context "when blank password attempts follow a valid login" do - def attempt_login(include_password) - password = include_password ? user.password : "" - clone_get path, user: user.username, password: password - response.status - end + context "when blank password attempts follow a valid login" do + def attempt_login(include_password) + password = include_password ? user.password : "" + clone_get path, user: user.username, password: password + response.status + end - it "repeated attempts followed by successful attempt" do - options = Gitlab.config.rack_attack.git_basic_auth - maxretry = options[:maxretry] - 1 - ip = '1.2.3.4' + it "repeated attempts followed by successful attempt" do + options = Gitlab.config.rack_attack.git_basic_auth + maxretry = options[:maxretry] - 1 + ip = '1.2.3.4' - allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) - Rack::Attack::Allow2Ban.reset(ip, options) + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + Rack::Attack::Allow2Ban.reset(ip, options) - maxretry.times.each do - expect(attempt_login(false)).to eq(401) - end + maxretry.times.each do + expect(attempt_login(false)).to eq(401) + end - expect(attempt_login(true)).to eq(200) - expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey + expect(attempt_login(true)).to eq(200) + expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey - maxretry.times.each do - expect(attempt_login(false)).to eq(401) - end + maxretry.times.each do + expect(attempt_login(false)).to eq(401) + end - Rack::Attack::Allow2Ban.reset(ip, options) + Rack::Attack::Allow2Ban.reset(ip, options) + end end end - end - context "when the user doesn't have access to the project" do - it "downloads get status 404" do - download(path, user: user.username, password: user.password) do |response| - expect(response).to have_http_status(404) + context "when the user doesn't have access to the project" do + it "downloads get status 404" do + download(path, user: user.username, password: user.password) do |response| + expect(response).to have_http_status(404) + end end - end - it "uploads get status 404" do - upload(path, user: user.username, password: user.password) do |response| - expect(response).to have_http_status(404) + it "uploads get status 404" do + upload(path, user: user.username, password: user.password) do |response| + expect(response).to have_http_status(404) + end end end end end - end - - context "when a gitlab ci token is provided" do - let(:build) { create(:ci_build, :running) } - let(:project) { build.project } - let(:other_project) { create(:empty_project) } - - before do - project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) - end - - context 'when build created by system is authenticated' do - it "downloads get status 200" do - 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 "uploads get status 401 (no project existence information leak)" do - push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token - - expect(response).to have_http_status(401) - end - - it "downloads from other project get status 404" do - clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token - expect(response).to have_http_status(404) - end - end + context "when a gitlab ci token is provided" do + let(:build) { create(:ci_build, :running) } + let(:project) { build.project } + let(:other_project) { create(:empty_project) } - context 'and build created by' do before do - build.update(user: user) - project.team << [user, :reporter] + project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) end - shared_examples 'can download code only' do - it 'downloads get status 200' do + context 'when build created by system is authenticated' do + it "downloads get status 200" do 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 'uploads get status 403' do + it "uploads get status 401 (no project existence information leak)" do push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token expect(response).to have_http_status(401) end + + it "downloads from other project get status 404" do + clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token + + expect(response).to have_http_status(404) + end end - context 'administrator' do - let(:user) { create(:admin) } + context 'and build created by' do + before do + build.update(user: user) + project.team << [user, :reporter] + end - it_behaves_like 'can download code only' + 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 - it 'downloads from other project get status 403' do - clone_get "#{other_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 - expect(response).to have_http_status(403) + it 'uploads get status 403' do + push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token + + expect(response).to have_http_status(401) + end end - end - context 'regular user' do - let(:user) { create(:user) } + context 'administrator' do + let(:user) { create(:admin) } - it_behaves_like 'can download code only' + it_behaves_like 'can download code only' - it 'downloads from other project get status 404' do - clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token + it 'downloads from other project get status 403' do + clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token - expect(response).to have_http_status(404) + expect(response).to have_http_status(403) + end + end + + context 'regular user' do + let(:user) { create(:user) } + + it_behaves_like 'can download code only' + + it 'downloads from other project get status 404' do + clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token + + expect(response).to have_http_status(404) + end end end end end end - end - context "when the project path doesn't end in .git" do - context "GET info/refs" do - let(:path) { "/#{project.path_with_namespace}/info/refs" } + context "when the project path doesn't end in .git" do + context "GET info/refs" do + let(:path) { "/#{project.path_with_namespace}/info/refs" } - context "when no params are added" do - before { get path } + context "when no params are added" do + before { get path } - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs") + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs") + end end - end - context "when the upload-pack service is requested" do - let(:params) { { service: 'git-upload-pack' } } - before { get path, params } + context "when the upload-pack service is requested" do + let(:params) { { service: 'git-upload-pack' } } + before { get path, params } - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") + end end - end - context "when the receive-pack service is requested" do - let(:params) { { service: 'git-receive-pack' } } - before { get path, params } + context "when the receive-pack service is requested" do + let(:params) { { service: 'git-receive-pack' } } + before { get path, params } - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") + end end - end - context "when the params are anything else" do - let(:params) { { service: 'git-implode-pack' } } - before { get path, params } + context "when the params are anything else" do + let(:params) { { service: 'git-implode-pack' } } + before { get path, params } - it "redirects to the sign-in page" do - expect(response).to redirect_to(new_user_session_path) + it "redirects to the sign-in page" do + expect(response).to redirect_to(new_user_session_path) + end end end - end - context "POST git-upload-pack" do - it "fails to find a route" do - expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) + context "POST git-upload-pack" do + it "fails to find a route" do + expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) + end end - end - context "POST git-receive-pack" do - it "failes to find a route" do - expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) + context "POST git-receive-pack" do + it "failes to find a route" do + expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) + end end end - end - context "retrieving an info/refs file" do - before { project.update_attribute(:visibility_level, Project::PUBLIC) } + context "retrieving an info/refs file" do + before { project.update_attribute(:visibility_level, Project::PUBLIC) } - context "when the file exists" do - before do - # Provide a dummy file in its place - allow_any_instance_of(Repository).to receive(:blob_at).and_call_original - allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do - Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore') - end + context "when the file exists" do + before do + # Provide a dummy file in its place + allow_any_instance_of(Repository).to receive(:blob_at).and_call_original + allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do + Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore') + end - get "/#{project.path_with_namespace}/blob/master/info/refs" - end + get "/#{project.path_with_namespace}/blob/master/info/refs" + end - it "returns the file" do - expect(response).to have_http_status(200) + it "returns the file" do + expect(response).to have_http_status(200) + end end - end - context "when the file does not exist" do - before { get "/#{project.path_with_namespace}/blob/master/info/refs" } + context "when the file does not exist" do + before { get "/#{project.path_with_namespace}/blob/master/info/refs" } - it "returns not found" do - expect(response).to have_http_status(404) + it "returns not found" do + expect(response).to have_http_status(404) + end end end end - def clone_get(project, options = {}) - get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token)) - end - - def clone_post(project, options = {}) - post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token)) - end - - def push_get(project, options = {}) - get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token)) - end - - def push_post(project, options = {}) - post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token)) - end + describe "User with LDAP identity" do + let(:user) { create(:omniauth_user, extern_uid: dn) } + let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } - def download(project, user: nil, password: nil, spnego_request_token: nil) - args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }] + before do + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow(Gitlab::LDAP::Authentication).to receive(:login).and_return(nil) + allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user) + end - clone_get(*args) - yield response + context "when authentication fails" do + context "when no authentication is provided" do + it "responds with status 401" do + download('doesnt/exist.git') do |response| + expect(response).to have_http_status(401) + end + end + end - clone_post(*args) - yield response - end + context "when username and invalid password are provided" do + it "responds with status 401" do + download('doesnt/exist.git', user: user.username, password: "nope") do |response| + expect(response).to have_http_status(401) + end + end + end + end - def upload(project, user: nil, password: nil, spnego_request_token: nil) - args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }] + context "when authentication succeeds" do + context "when the project doesn't exist" do + it "responds with status 404" do + download('/doesnt/exist.git', user: user.username, password: user.password) do |response| + expect(response).to have_http_status(404) + end + end + end - push_get(*args) - yield response + context "when the project exists" do + let(:project) { create(:project, path: 'project.git-project') } - push_post(*args) - yield response - end + before do + project.team << [user, :master] + end - def auth_env(user, password, spnego_request_token) - env = workhorse_internal_api_request_header - if user && password - env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password) - elsif spnego_request_token - env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}" + it "responds with status 200" do + clone_get(path, user: user.username, password: user.password) do |response| + expect(response).to have_http_status(200) + end + end + end end - - env end end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 6b956e63004..f0ef155bd7b 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -39,7 +39,7 @@ describe JwtController do subject! { get '/jwt/auth', parameters, headers } - it { expect(response).to have_http_status(403) } + it { expect(response).to have_http_status(401) } end end @@ -77,7 +77,7 @@ describe JwtController do subject! { get '/jwt/auth', parameters, headers } - it { expect(response).to have_http_status(403) } + it { expect(response).to have_http_status(401) } end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 09e4e265dd1..dbdf83a0dff 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -257,6 +257,29 @@ describe 'Git LFS API and storage' do it_behaves_like 'responds with a file' end + describe 'when using a user key' do + let(:authorization) { authorize_user_key } + + context 'when user allowed' do + let(:update_permissions) do + project.team << [user, :master] + project.lfs_objects << lfs_object + end + + it_behaves_like 'responds with a file' + end + + context 'when user not allowed' do + let(:update_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with status 404' do + expect(response).to have_http_status(404) + end + end + end + context 'when build is authorized as' do let(:authorization) { authorize_ci_project } @@ -1110,7 +1133,11 @@ describe 'Git LFS API and storage' do end def authorize_deploy_key - ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate) + ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token) + end + + def authorize_user_key + ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token) end def fork_project(project, user, object = nil) diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index cf4c5f13635..e65da15aca8 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,10 +13,10 @@ describe Boards::Issues::ListService, services: true do let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } - let!(:backlog) { create(:backlog_list, board: board) } + let!(:backlog) { project.board.backlog_list } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } - let!(:done) { create(:done_list, board: board) } + let!(:done) { project.board.done_list } let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 0122159cab8..180f1b08631 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -10,10 +10,10 @@ describe Boards::Issues::MoveService, services: true do let(:development) { create(:label, project: project, name: 'Development') } let(:testing) { create(:label, project: project, name: 'Testing') } - let!(:backlog) { create(:backlog_list, board: board) } + let!(:backlog) { project.board.backlog_list } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } - let!(:done) { create(:done_list, board: board) } + let!(:done) { project.board.done_list } before do project.team << [user, :developer] diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index 90764b86b16..bff9c1fd1fe 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -17,17 +17,15 @@ describe Boards::Lists::CreateService, services: true do end end - context 'when board lists has only a backlog list' do + context 'when board lists has backlog, and done lists' do it 'creates a new list at beginning of the list' do - create(:backlog_list, board: board) - list = service.execute expect(list.position).to eq 0 end end - context 'when board lists has only labels lists' do + context 'when board lists has labels lists' do it 'creates a new list at end of the lists' do create(:list, board: board, position: 0) create(:list, board: board, position: 1) @@ -40,8 +38,6 @@ describe Boards::Lists::CreateService, services: true do context 'when board lists has backlog, label and done lists' do it 'creates a new list at end of the label lists' do - create(:backlog_list, board: board) - create(:done_list, board: board) list1 = create(:list, board: board, position: 0) list2 = service.execute diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index 6eff445feee..474c4512471 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -15,11 +15,11 @@ describe Boards::Lists::DestroyService, services: true do end it 'decrements position of higher lists' do - backlog = create(:backlog_list, board: board) + backlog = project.board.backlog_list development = create(:list, board: board, position: 0) review = create(:list, board: board, position: 1) staging = create(:list, board: board, position: 2) - done = create(:done_list, board: board) + done = project.board.done_list described_class.new(project, user).execute(development) @@ -31,14 +31,14 @@ describe Boards::Lists::DestroyService, services: true do end it 'does not remove list from board when list type is backlog' do - list = create(:backlog_list, board: board) + list = project.board.backlog_list service = described_class.new(project, user) expect { service.execute(list) }.not_to change(board.lists, :count) end it 'does not remove list from board when list type is done' do - list = create(:done_list, board: board) + list = project.board.done_list service = described_class.new(project, user) expect { service.execute(list) }.not_to change(board.lists, :count) diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb index 3e9b7d07fc6..102ed67449d 100644 --- a/spec/services/boards/lists/move_service_spec.rb +++ b/spec/services/boards/lists/move_service_spec.rb @@ -6,12 +6,12 @@ describe Boards::Lists::MoveService, services: true do let(:board) { project.board } let(:user) { create(:user) } - let!(:backlog) { create(:backlog_list, board: board) } + let!(:backlog) { project.board.backlog_list } let!(:planning) { create(:list, board: board, position: 0) } let!(:development) { create(:list, board: board, position: 1) } let!(:review) { create(:list, board: board, position: 2) } let!(:staging) { create(:list, board: board, position: 3) } - let!(:done) { create(:done_list, board: board) } + let!(:done) { project.board.done_list } context 'when list type is set to label' do it 'keeps position of lists when new position is nil' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 8326e5cd313..ff113efd916 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -18,7 +18,7 @@ describe Ci::ProcessPipelineService, services: true do all_builds.where.not(status: [:created, :skipped]) end - def create_builds + def process_pipeline described_class.new(pipeline.project, user).execute(pipeline) end @@ -36,26 +36,26 @@ describe Ci::ProcessPipelineService, services: true do end it 'processes a pipeline' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy succeed_pending expect(builds.success.count).to eq(2) - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy succeed_pending expect(builds.success.count).to eq(4) - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy succeed_pending expect(builds.success.count).to eq(5) - expect(create_builds).to be_falsey + expect(process_pipeline).to be_falsey end it 'does not process pipeline if existing stage is running' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pending.count).to eq(2) - expect(create_builds).to be_falsey + expect(process_pipeline).to be_falsey expect(builds.pending.count).to eq(2) end end @@ -67,7 +67,7 @@ describe Ci::ProcessPipelineService, services: true do end it 'automatically triggers a next stage when build finishes' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:status)).to contain_exactly('pending') pipeline.builds.running_or_pending.each(&:drop) @@ -88,7 +88,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when builds are successful' do it 'properly creates builds' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:name)).to contain_exactly('build') expect(builds.pluck(:status)).to contain_exactly('pending') pipeline.builds.running_or_pending.each(&:success) @@ -113,7 +113,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when test job fails' do it 'properly creates builds' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:name)).to contain_exactly('build') expect(builds.pluck(:status)).to contain_exactly('pending') pipeline.builds.running_or_pending.each(&:success) @@ -138,7 +138,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when test and test_failure jobs fail' do it 'properly creates builds' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:name)).to contain_exactly('build') expect(builds.pluck(:status)).to contain_exactly('pending') pipeline.builds.running_or_pending.each(&:success) @@ -164,7 +164,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when deploy job fails' do it 'properly creates builds' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:name)).to contain_exactly('build') expect(builds.pluck(:status)).to contain_exactly('pending') pipeline.builds.running_or_pending.each(&:success) @@ -189,7 +189,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when build is canceled in the second stage' do it 'does not schedule builds after build has been canceled' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:name)).to contain_exactly('build') expect(builds.pluck(:status)).to contain_exactly('pending') pipeline.builds.running_or_pending.each(&:success) @@ -208,7 +208,7 @@ describe Ci::ProcessPipelineService, services: true do context 'when listing manual actions' do it 'returns only for skipped builds' do # currently all builds are created - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(manual_actions).to be_empty # succeed stage build @@ -230,6 +230,69 @@ describe Ci::ProcessPipelineService, services: true do end end + context 'when there are manual/on_failure jobs in earlier stages' do + before do + builds + process_pipeline + builds.each(&:reload) + end + + context 'when first stage has only manual jobs' do + let(:builds) do + [create_build('build', 0, 'manual'), + create_build('check', 1), + create_build('test', 2)] + end + + it 'starts from the second stage' do + expect(builds.map(&:status)).to eq(%w[skipped pending created]) + end + end + + context 'when second stage has only manual jobs' do + let(:builds) do + [create_build('check', 0), + create_build('build', 1, 'manual'), + create_build('test', 2)] + end + + it 'skips second stage and continues on third stage' do + expect(builds.map(&:status)).to eq(%w[pending created created]) + + builds.first.success + builds.each(&:reload) + + expect(builds.map(&:status)).to eq(%w[success skipped pending]) + end + end + + context 'when second stage has only on_failure jobs' do + let(:builds) do + [create_build('check', 0), + create_build('build', 1, 'on_failure'), + create_build('test', 2)] + end + + it 'skips second stage and continues on third stage' do + expect(builds.map(&:status)).to eq(%w[pending created created]) + + builds.first.success + builds.each(&:reload) + + expect(builds.map(&:status)).to eq(%w[success skipped pending]) + end + end + + def create_build(name, stage_idx, when_value = nil) + create(:ci_build, + :created, + pipeline: pipeline, + name: name, + stage_idx: stage_idx, + when: when_value) + end + end + context 'when failed build in the middle stage is retried' do context 'when failed build is the only unsuccessful build in the stage' do before do @@ -242,7 +305,7 @@ describe Ci::ProcessPipelineService, services: true do end it 'does trigger builds in the next stage' do - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2') pipeline.builds.running_or_pending.each(&:success) @@ -297,14 +360,14 @@ describe Ci::ProcessPipelineService, services: true do expect(all_builds.count).to eq(2) # Create builds will mark the created as pending - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.count).to eq(2) expect(all_builds.count).to eq(2) # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy) succeed_pending - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.success.count).to eq(2) expect(builds.pending.count).to eq(2) expect(all_builds.count).to eq(5) @@ -312,14 +375,14 @@ describe Ci::ProcessPipelineService, services: true do # When we succeed the 2 pending from stage test, # We will queue a deploy stage, no new builds will be created succeed_pending - expect(create_builds).to be_truthy + expect(process_pipeline).to be_truthy expect(builds.pending.count).to eq(1) expect(builds.success.count).to eq(4) expect(all_builds.count).to eq(5) # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created succeed_pending - expect(create_builds).to be_falsey + expect(process_pipeline).to be_falsey expect(builds.success.count).to eq(5) expect(all_builds.count).to eq(5) end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 58569ba96c3..1050502fa19 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -20,16 +20,38 @@ describe Issues::CreateService, services: true do let(:opts) do { title: 'Awesome issue', description: 'please fix', - assignee: assignee, + assignee_id: assignee.id, label_ids: labels.map(&:id), - milestone_id: milestone.id } + milestone_id: milestone.id, + due_date: Date.tomorrow } end - it { expect(issue).to be_valid } - it { expect(issue.title).to eq('Awesome issue') } - it { expect(issue.assignee).to eq assignee } - it { expect(issue.labels).to match_array labels } - it { expect(issue.milestone).to eq milestone } + it 'creates the issue with the given params' do + expect(issue).to be_persisted + expect(issue.title).to eq('Awesome issue') + expect(issue.assignee).to eq assignee + expect(issue.labels).to match_array labels + expect(issue.milestone).to eq milestone + expect(issue.due_date).to eq Date.tomorrow + end + + context 'when current user cannot admin issues in the project' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + end + + it 'filters out params that cannot be set without the :admin_issue permission' do + issue = described_class.new(project, guest, opts).execute + + expect(issue).to be_persisted + expect(issue.title).to eq('Awesome issue') + expect(issue.assignee).to be_nil + expect(issue.labels).to be_empty + expect(issue.milestone).to be_nil + expect(issue.due_date).to be_nil + end + end it 'creates a pending todo for new assignee' do attributes = { diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 4f5375a3583..1638a46ed51 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -32,55 +32,84 @@ describe Issues::UpdateService, services: true do described_class.new(project, user, opts).execute(issue) end - context "valid params" do - before do - opts = { + context 'valid params' do + let(:opts) do + { title: 'New title', description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id] + label_ids: [label.id], + due_date: Date.tomorrow } - - perform_enqueued_jobs do - update_issue(opts) - end end - it { expect(issue).to be_valid } - it { expect(issue.title).to eq('New title') } - it { expect(issue.assignee).to eq(user2) } - it { expect(issue).to be_closed } - it { expect(issue.labels.count).to eq(1) } - it { expect(issue.labels.first.title).to eq(label.name) } - - it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do - deliveries = ActionMailer::Base.deliveries - email = deliveries.last - recipients = deliveries.last(2).map(&:to).flatten - expect(recipients).to include(user2.email, user3.email) - expect(email.subject).to include(issue.title) + it 'updates the issue with the given params' do + update_issue(opts) + + expect(issue).to be_valid + expect(issue.title).to eq 'New title' + expect(issue.description).to eq 'Also please fix' + expect(issue.assignee).to eq user2 + expect(issue).to be_closed + expect(issue.labels).to match_array [label] + expect(issue.due_date).to eq Date.tomorrow end - it 'creates system note about issue reassign' do - note = find_note('Reassigned to') + context 'when current user cannot admin issues in the project' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + end - expect(note).not_to be_nil - expect(note.note).to include "Reassigned to \@#{user2.username}" + it 'filters out params that cannot be set without the :admin_issue permission' do + described_class.new(project, guest, opts).execute(issue) + + expect(issue).to be_valid + expect(issue.title).to eq 'New title' + expect(issue.description).to eq 'Also please fix' + expect(issue.assignee).to eq user3 + expect(issue.labels).to be_empty + expect(issue.milestone).to be_nil + expect(issue.due_date).to be_nil + end end - it 'creates system note about issue label edit' do - note = find_note('Added ~') + context 'with background jobs processed' do + before do + perform_enqueued_jobs do + update_issue(opts) + end + end + + it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do + deliveries = ActionMailer::Base.deliveries + email = deliveries.last + recipients = deliveries.last(2).map(&:to).flatten + expect(recipients).to include(user2.email, user3.email) + expect(email.subject).to include(issue.title) + end - expect(note).not_to be_nil - expect(note.note).to include "Added ~#{label.id} label" - end + it 'creates system note about issue reassign' do + note = find_note('Reassigned to') - it 'creates system note about title change' do - note = find_note('Changed title:') + expect(note).not_to be_nil + expect(note.note).to include "Reassigned to \@#{user2.username}" + end - expect(note).not_to be_nil - expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' + it 'creates system note about issue label edit' do + note = find_note('Added ~') + + expect(note).not_to be_nil + expect(note.note).to include "Added ~#{label.id} label" + end + + it 'creates system note about title change' do + note = find_note('Changed title:') + + expect(note).not_to be_nil + expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' + end end end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb new file mode 100644 index 00000000000..03e296259f9 --- /dev/null +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Members::ApproveAccessRequestService, services: true do + let(:user) { create(:user) } + let(:access_requester) { create(:user) } + let(:project) { create(:project, :public) } + let(:group) { create(:group, :public) } + + shared_examples 'a service raising ActiveRecord::RecordNotFound' do + it 'raises ActiveRecord::RecordNotFound' do + expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do + it 'raises Gitlab::Access::AccessDeniedError' do + expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + + shared_examples 'a service approving an access request' do + it 'succeeds' do + expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1) + end + + it 'returns a <Source>Member' do + member = described_class.new(source, user, params).execute + + expect(member).to be_a "#{source.class}Member".constantize + expect(member.requested_at).to be_nil + end + + context 'with a custom access level' do + let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } } + + it 'returns a ProjectMember with the custom access level' do + member = described_class.new(source, user, params).execute + + expect(member.access_level).to eq Gitlab::Access::MASTER + end + end + end + + context 'when no access requester are found' do + let(:params) { { user_id: 42 } } + + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do + let(:source) { project } + end + + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do + let(:source) { group } + end + end + + context 'when an access requester is found' do + before do + project.request_access(access_requester) + group.request_access(access_requester) + end + let(:params) { { user_id: access_requester.id } } + + context 'when current user cannot approve access request to the project' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'when current user can approve access request to the project' do + before do + project.team << [user, :master] + group.add_owner(user) + end + + it_behaves_like 'a service approving an access request' do + let(:source) { project } + end + + it_behaves_like 'a service approving an access request' do + let(:source) { group } + end + + context 'when given a :id' do + let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } } + + it_behaves_like 'a service approving an access request' do + let(:source) { project } + end + end + end + end +end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 2395445e7fd..9995f3488af 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -2,70 +2,111 @@ require 'spec_helper' describe Members::DestroyService, services: true do let(:user) { create(:user) } - let(:project) { create(:project) } - let!(:member) { create(:project_member, source: project) } + let(:member_user) { create(:user) } + let(:project) { create(:project, :public) } + let(:group) { create(:group, :public) } - context 'when member is nil' do - before do - project.team << [user, :developer] + shared_examples 'a service raising ActiveRecord::RecordNotFound' do + it 'raises ActiveRecord::RecordNotFound' do + expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) end + end - it 'does not destroy the member' do - expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError) + shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do + it 'raises Gitlab::Access::AccessDeniedError' do + expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) end end - context 'when current user cannot destroy the given member' do - before do - project.team << [user, :developer] + shared_examples 'a service destroying a member' do + it 'destroys the member' do + expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1) + end + + context 'when the given member is an access requester' do + before do + source.members.find_by(user_id: member_user).destroy + source.request_access(member_user) + end + let(:access_requester) { source.requesters.find_by(user_id: member_user) } + + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' + + %i[requesters all].each do |scope| + context "and #{scope} scope is passed" do + it 'destroys the access requester' do + expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1) + end + + it 'calls Member#after_decline_request' do + expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester) + + described_class.new(source, user, params).execute(scope) + end + + context 'when current user is the member' do + it 'does not call Member#after_decline_request' do + expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester) + + described_class.new(source, member_user, params).execute(scope) + end + end + end + end end + end + + context 'when no member are found' do + let(:params) { { user_id: 42 } } - it 'does not destroy the member' do - expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError) + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do + let(:source) { project } + end + + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do + let(:source) { group } end end - context 'when current user can destroy the given member' do + context 'when a member is found' do before do - project.team << [user, :master] + project.team << [member_user, :developer] + group.add_developer(member_user) end + let(:params) { { user_id: member_user.id } } - it 'destroys the member' do - destroy_member(member, user) + context 'when current user cannot destroy the given member' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end - expect(member).to be_destroyed + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end end - context 'when the given member is a requester' do + context 'when current user can destroy the given member' do before do - member.update_column(:requested_at, Time.now) + project.team << [user, :master] + group.add_owner(user) end - it 'calls Member#after_decline_request' do - expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) - - destroy_member(member, user) + it_behaves_like 'a service destroying a member' do + let(:source) { project } end - context 'when current user is the member' do - it 'does not call Member#after_decline_request' do - expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) - - destroy_member(member, member.user) - end + it_behaves_like 'a service destroying a member' do + let(:source) { group } end - context 'when current user is the member and ' do - it 'does not call Member#after_decline_request' do - expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) + context 'when given a :id' do + let(:params) { { id: project.members.find_by!(user_id: user.id).id } } - destroy_member(member, member.user) + it 'destroys the member' do + expect { described_class.new(project, user, params).execute }. + to change { project.members.count }.by(-1) end end end end - - def destroy_member(member, user) - Members::DestroyService.new(member, user).execute - end end diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb new file mode 100644 index 00000000000..0d2d5f03199 --- /dev/null +++ b/spec/services/members/request_access_service_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Members::RequestAccessService, services: true do + let(:user) { create(:user) } + let(:project) { create(:project, :private) } + let(:group) { create(:group, :private) } + + shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do + it 'raises Gitlab::Access::AccessDeniedError' do + expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + + shared_examples 'a service creating a access request' do + it 'succeeds' do + expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1) + end + + it 'returns a <Source>Member' do + member = described_class.new(source, user).execute + + expect(member).to be_a "#{source.class}Member".constantize + expect(member.requested_at).to be_present + end + end + + context 'when source is nil' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { nil } + end + end + + context 'when current user cannot request access to the project' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'when current user can request access to the project' do + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it_behaves_like 'a service creating a access request' do + let(:source) { project } + end + + it_behaves_like 'a service creating a access request' do + let(:source) { group } + end + end +end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 159f6817e8d..e49a0d5e553 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -38,6 +38,66 @@ describe MergeRequests::MergeService, services: true do end end + context 'closes related issues' do + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } + + before do + allow(project).to receive(:default_branch).and_return(merge_request.target_branch) + end + + it 'closes GitLab issue tracker issues' do + issue = create :issue, project: project + commit = double('commit', safe_message: "Fixes #{issue.to_reference}") + allow(merge_request).to receive(:commits).and_return([commit]) + + service.execute(merge_request) + + expect(issue.reload.closed?).to be_truthy + end + + context 'with JIRA integration' do + include JiraServiceHelper + + let(:jira_tracker) { project.create_jira_service } + + before { jira_service_settings } + + it 'closes issues on JIRA issue tracker' do + jira_issue = ExternalIssue.new('JIRA-123', project) + commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") + allow(merge_request).to receive(:commits).and_return([commit]) + + expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once + + service.execute(merge_request) + end + end + end + + context 'closes related todos' do + let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:project) { merge_request.project } + let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } + let!(:todo) do + create(:todo, :assigned, + project: project, + author: user, + user: user, + target: merge_request) + end + + before do + allow(service).to receive(:execute_hooks) + + perform_enqueued_jobs do + service.execute(merge_request) + todo.reload + end + end + + it { expect(todo).to be_done } + end + context 'remove source branch by author' do let(:service) do merge_request.merge_params['force_remove_source_branch'] = '1' diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index a162df5fc34..59d3912018a 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -79,8 +79,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it { expect(@build_failed_todo).to be_done } + it { expect(@fork_build_failed_todo).to be_done } end context 'manual merge of source branch' do @@ -99,8 +99,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it { expect(@build_failed_todo).to be_done } + it { expect(@fork_build_failed_todo).to be_done } end context 'push to fork repo source branch' do @@ -149,8 +149,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it { expect(@build_failed_todo).to be_done } + it { expect(@fork_build_failed_todo).to be_done } end context 'push new branch that exists in a merge request' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 0d152534c38..d820646ebdf 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -962,6 +962,20 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end + it "notifies the merger when merge_when_build_succeeds is true" do + merge_request.merge_when_build_succeeds = true + notification.merge_mr(merge_request, @u_watcher) + + should_email(@u_watcher) + end + + it "does not notify the merger when merge_when_build_succeeds is false" do + merge_request.merge_when_build_succeeds = false + notification.merge_mr(merge_request, @u_watcher) + + should_not_email(@u_watcher) + end + context 'participating' do context 'by assignee' do before do diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 29341c5e57e..7dcd03496bb 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do let!(:project) { create(:project, namespace: user.namespace) } let!(:path) { project.repository.path_to_repo } let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } + let!(:async) { false } # execute or async_execute context 'Sidekiq inline' do before do @@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end + context 'async delete of project with private issue visibility' do + let!(:async) { true } + + before do + project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE) + # Run sidekiq immediately to check that renamed repository will be removed + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end + + it 'deletes the project' do + expect(Project.all).not_to include(project) + expect(Dir.exist?(path)).to be_falsey + expect(Dir.exist?(remove_path)).to be_falsey + end + end + context 'container registry' do before do stub_container_registry_config(enabled: true) @@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do end def destroy_project(project, user, params) - Projects::DestroyService.new(project, user, params).execute + if async + Projects::DestroyService.new(project, user, params).async_execute + else + Projects::DestroyService.new(project, user, params).execute + end end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index d5d4d7c56ef..ed1384798ab 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -108,6 +108,16 @@ describe Projects::ImportService, services: true do expect(result[:status]).to eq :error expect(result[:message]).to eq 'Github: failed to connect API' end + + it 'expires existence cache after error' do + allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true) + + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original + expect_any_instance_of(Repository).to receive(:expire_exists_cache).and_call_original + + subject.execute + end end def stub_github_omniauth_provider diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index a616275e883..ae4d286d250 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' describe SlashCommands::InterpretService, services: true do - let(:project) { create(:project) } - let(:user) { create(:user) } + let(:project) { create(:empty_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') } before do - project.team << [user, :developer] + project.team << [developer, :developer] end describe '#execute' do - let(:service) { described_class.new(project, user) } + let(:service) { described_class.new(project, developer) } let(:merge_request) { create(:merge_request, source_project: project) } shared_examples 'reopen command' do @@ -45,13 +45,13 @@ describe SlashCommands::InterpretService, services: true do it 'fetches assignee and populates assignee_id if content contains /assign' do _, updates = service.execute(content, issuable) - expect(updates).to eq(assignee_id: user.id) + expect(updates).to eq(assignee_id: developer.id) end end shared_examples 'unassign command' do it 'populates assignee_id: nil if content contains /unassign' do - issuable.update(assignee_id: user.id) + issuable.update(assignee_id: developer.id) _, updates = service.execute(content, issuable) expect(updates).to eq(assignee_id: nil) @@ -124,7 +124,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'done command' do it 'populates todo_event: "done" if content contains /done' do - TodoService.new.mark_todo(issuable, user) + TodoService.new.mark_todo(issuable, developer) _, updates = service.execute(content, issuable) expect(updates).to eq(todo_event: 'done') @@ -141,7 +141,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'unsubscribe command' do it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do - issuable.subscribe(user) + issuable.subscribe(developer) _, updates = service.execute(content, issuable) expect(updates).to eq(subscription_event: 'unsubscribe') @@ -165,6 +165,23 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'wip command' do + it 'returns wip_event: "wip" if content contains /wip' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(wip_event: 'wip') + end + end + + shared_examples 'unwip command' do + it 'returns wip_event: "unwip" if content contains /wip' do + issuable.update(title: issuable.wip_title) + _, updates = service.execute(content, issuable) + + expect(updates).to eq(wip_event: 'unwip') + end + end + shared_examples 'empty command' do it 'populates {} if content contains an unsupported command' do _, updates = service.execute(content, issuable) @@ -209,12 +226,12 @@ describe SlashCommands::InterpretService, services: true do end it_behaves_like 'assign command' do - let(:content) { "/assign @#{user.username}" } + let(:content) { "/assign @#{developer.username}" } let(:issuable) { issue } end it_behaves_like 'assign command' do - let(:content) { "/assign @#{user.username}" } + let(:content) { "/assign @#{developer.username}" } let(:issuable) { merge_request } end @@ -376,9 +393,70 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { issue } end + it_behaves_like 'wip command' do + let(:content) { '/wip' } + let(:issuable) { merge_request } + end + + it_behaves_like 'unwip command' do + let(:content) { '/wip' } + let(:issuable) { merge_request } + end + it_behaves_like 'empty command' do let(:content) { '/remove_due_date' } let(:issuable) { merge_request } end + + context 'when current_user cannot :admin_issue' do + let(:visitor) { create(:user) } + let(:issue) { create(:issue, project: project, author: visitor) } + let(:service) { described_class.new(project, visitor) } + + it_behaves_like 'empty command' do + let(:content) { "/assign @#{developer.username}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/unassign' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { "/milestone %#{milestone.title}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/remove_milestone' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { %(/unlabel ~"#{inprogress.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { %(/relabel ~"#{inprogress.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/due tomorrow' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/remove_due_date' } + let(:issuable) { issue } + end + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a3f16e2d945..c22dd9ab77a 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -40,6 +40,12 @@ describe SystemNoteService, services: true do describe 'note body' do let(:note_lines) { subject.note.split("\n").reject(&:blank?) } + describe 'comparison diff link line' do + it 'adds the comparison text' do + expect(note_lines[2]).to match "[Compare with previous version]" + end + end + context 'without existing commits' do it 'adds a message header' do expect(note_lines[0]).to eq "Added #{new_commits.size} commits:" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 02b2b3ca101..b19f5824236 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,7 +26,7 @@ RSpec.configure do |config| config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::TestHelpers, type: :controller + config.include Devise::Test::ControllerHelpers, type: :controller config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature config.include StubConfiguration diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index e8e760a6187..62a5b46d47b 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -4,24 +4,28 @@ module CycleAnalyticsHelpers create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name) end - def create_commit(message, project, user, branch_name) - filename = random_git_name + def create_commit(message, project, user, branch_name, count: 1) oldrev = project.repository.commit(branch_name).sha + commit_shas = Array.new(count) do |index| + filename = random_git_name - options = { - committer: project.repository.user_to_committer(user), - author: project.repository.user_to_committer(user), - commit: { message: message, branch: branch_name, update_ref: true }, - file: { content: "content", path: filename, update: false } - } + options = { + committer: project.repository.user_to_committer(user), + author: project.repository.user_to_committer(user), + commit: { message: message, branch: branch_name, update_ref: true }, + file: { content: "content", path: filename, update: false } + } + + commit_sha = Gitlab::Git::Blob.commit(project.repository, options) + project.repository.commit(commit_sha) - commit_sha = Gitlab::Git::Blob.commit(project.repository, options) - project.repository.commit(commit_sha) + commit_sha + end GitPushService.new(project, user, oldrev: oldrev, - newrev: commit_sha, + newrev: commit_shas.last, ref: 'refs/heads/master').execute end diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 5e3b8f2b23e..5e3b8f2b23e 100644 --- a/spec/support/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb diff --git a/spec/support/git_http_helpers.rb b/spec/support/git_http_helpers.rb new file mode 100644 index 00000000000..46b686fce94 --- /dev/null +++ b/spec/support/git_http_helpers.rb @@ -0,0 +1,48 @@ +module GitHttpHelpers + def clone_get(project, options = {}) + get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token)) + end + + def clone_post(project, options = {}) + post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token)) + end + + def push_get(project, options = {}) + get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token)) + end + + def push_post(project, options = {}) + post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token)) + end + + def download(project, user: nil, password: nil, spnego_request_token: nil) + args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }] + + clone_get(*args) + yield response + + clone_post(*args) + yield response + end + + def upload(project, user: nil, password: nil, spnego_request_token: nil) + args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }] + + push_get(*args) + yield response + + push_post(*args) + yield response + end + + def auth_env(user, password, spnego_request_token) + env = workhorse_internal_api_request_header + if user && password + env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password) + elsif spnego_request_token + env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}" + end + + env + end +end diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb new file mode 100644 index 00000000000..02605d6b70e --- /dev/null +++ b/spec/support/matchers/have_issuable_counts.rb @@ -0,0 +1,21 @@ +RSpec::Matchers.define :have_issuable_counts do |opts| + match do |actual| + expected_counts = opts.map do |state, count| + "#{state.to_s.humanize} #{count}" + end + + actual.within '.issues-state-filters' do + expected_counts.each do |expected_count| + expect(actual).to have_content(expected_count) + end + end + end + + description do + "displays the following issuable counts: #{expected_counts.inspect}" + end + + failure_message do + "expected the following issuable counts: #{expected_counts.inspect} to be displayed" + end +end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index e876d44c166..f57c82809a6 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -9,7 +9,7 @@ shared_context 'mentionable context' do let(:author) { subject.author } let(:mentioned_issue) { create(:issue, project: project) } - let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } + let!(:mentioned_mr) { create(:merge_request, source_project: project) } let(:mentioned_commit) { project.commit("HEAD~1") } let(:ext_proj) { create(:project, :public) } @@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do subject.save + subject.create_cross_references! new_text = <<-MSG.strip_heredoc These references already existed: @@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do end # These two issues are new and should receive reference notes + # In the case of MergeRequests remember that cannot mention commits included in the MergeRequest new_issues.each do |newref| expect(SystemNoteService).to receive(:cross_reference). with(newref, subject.local_reference, author) diff --git a/spec/support/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index 5f9645ed44f..5f9645ed44f 100644 --- a/spec/support/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/snippets_shared_examples.rb new file mode 100644 index 00000000000..57dfff3471f --- /dev/null +++ b/spec/support/snippets_shared_examples.rb @@ -0,0 +1,18 @@ +# These shared examples expect a `snippets` array of snippets +RSpec.shared_examples 'paginated snippets' do |remote: false| + it "is limited to #{Snippet.default_per_page} items per page" do + expect(page.all('.snippets-list-holder .snippet-row').count).to eq(Snippet.default_per_page) + end + + context 'clicking on the link to the second page' do + before do + click_link('2') + wait_for_ajax if remote + end + + it 'shows the remaining snippets' do + remaining_snippets_count = [snippets.size - Snippet.default_per_page, Snippet.default_per_page].min + expect(page).to have_selector('.snippets-list-holder .snippet-row', count: remaining_snippets_count) + end + end +end diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb index dae858a52f6..68d2d72876e 100644 --- a/spec/views/admin/dashboard/index.html.haml_spec.rb +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'admin/dashboard/index.html.haml' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers before do assign(:projects, create_list(:empty_project, 1)) diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb index 3a65a86cd88..2dac5ee23c8 100644 --- a/spec/views/ci/lints/show.html.haml_spec.rb +++ b/spec/views/ci/lints/show.html.haml_spec.rb @@ -3,33 +3,95 @@ require 'spec_helper' describe 'ci/lints/show' do include Devise::TestHelpers - before do - assign(:status, true) - assign(:stages, %w[test]) - assign(:builds, builds) + describe 'XSS protection' do + let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) } + before do + assign(:status, true) + assign(:builds, config_processor.builds) + assign(:stages, config_processor.stages) + assign(:jobs, config_processor.jobs) + end + + context 'when builds attrbiutes contain HTML nodes' do + let(:content) do + { + rspec: { + script: '<h1>rspec</h1>', + stage: 'test' + } + } + end + + it 'does not render HTML elements' do + render + + expect(rendered).not_to have_css('h1', text: 'rspec') + end + end + + context 'when builds attributes do not contain HTML nodes' do + let(:content) do + { + rspec: { + script: 'rspec', + stage: 'test' + } + } + end + + it 'shows configuration in the table' do + render + + expect(rendered).to have_css('td pre', text: 'rspec') + end + end end - context 'when builds attrbiutes contain HTML nodes' do - let(:builds) do - [ { name: 'rspec', stage: 'test', commands: '<h1>rspec</h1>' } ] + let(:content) do + { + build_template: { + script: './build.sh', + tags: ['dotnet'], + only: ['test@dude/repo'], + except: ['deploy'], + environment: 'testing' + } + } + end + + let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) } + + context 'when the content is valid' do + before do + assign(:status, true) + assign(:builds, config_processor.builds) + assign(:stages, config_processor.stages) + assign(:jobs, config_processor.jobs) end - it 'does not render HTML elements' do + it 'shows the correct values' do render - expect(rendered).not_to have_css('h1', text: 'rspec') + expect(rendered).to have_content('Tag list: dotnet') + expect(rendered).to have_content('Refs only: test@dude/repo') + expect(rendered).to have_content('Refs except: deploy') + expect(rendered).to have_content('Environment: testing') + expect(rendered).to have_content('When: on_success') end end - context 'when builds attributes do not contain HTML nodes' do - let(:builds) do - [ { name: 'rspec', stage: 'test', commands: 'rspec' } ] + context 'when the content is invalid' do + before do + assign(:status, false) + assign(:error, 'Undefined error') end - it 'shows configuration in the table' do + it 'shows error message' do render - expect(rendered).to have_css('td pre', text: 'rspec') + expect(rendered).to have_content('Status: syntax is incorrect') + expect(rendered).to have_content('Error: Undefined error') + expect(rendered).not_to have_content('Tag list:') end end end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 446ba3bfa14..da43622d3f9 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/builds/show' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:project) { create(:project) } let(:pipeline) do diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb index 78af61f15a7..c8a3d02d8fd 100644 --- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/issues/_related_branches' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:project) { create(:project) } let(:branch) { project.repository.find_branch('feature') } diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb index 21f49d396e7..86980f59cd8 100644 --- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/merge_requests/widget/_heading' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers context 'when released to an environment' do let(:project) { merge_request.target_project } diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 31bbb150698..3650b22c389 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -1,18 +1,21 @@ require 'spec_helper' describe 'projects/merge_requests/edit.html.haml' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:user) { create(:user) } let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:milestone) { create(:milestone, project: project) } let(:closed_merge_request) do create(:closed_merge_request, source_project: fork_project, target_project: project, - author: user) + author: user, + assignee: user, + milestone: milestone) end before do 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 fe0780e72df..33cabd14913 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/merge_requests/show.html.haml' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:user) { create(:user) } let(:project) { create(:project) } @@ -41,4 +41,17 @@ describe 'projects/merge_requests/show.html.haml' do expect(rendered).to have_css('a', visible: false, text: 'Close') end end + + context 'when the merge request is open' do + it 'closes the merge request if the source project does not exist' do + closed_merge_request.update_attributes(state: 'open') + fork_project.destroy + + render + + expect(closed_merge_request.reload.state).to eq('closed') + expect(rendered).to have_css('a', visible: false, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + end end diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb index 932d6756ad2..b14b1ece2d0 100644 --- a/spec/views/projects/notes/_form.html.haml_spec.rb +++ b/spec/views/projects/notes/_form.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/notes/_form' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:user) { create(:user) } let(:project) { create(:empty_project) } diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index ac7f3ffb157..bf027499c94 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/pipelines/show' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb index 0f3fc1ee1ac..c381b1a86df 100644 --- a/spec/views/projects/tree/show.html.haml_spec.rb +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'projects/tree/show' do - include Devise::TestHelpers + include Devise::Test::ControllerHelpers let(:project) { create(:project) } let(:repository) { project.repository } diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 1d2cf7acddd..ffeaafe654a 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -79,7 +79,9 @@ describe PostReceive do end it "does not run if the author is not in the project" do - allow(Key).to receive(:find_by).with(hash_including(id: anything())) { nil } + allow_any_instance_of(Gitlab::GitPostReceive). + to receive(:identify_using_ssh_key). + and_return(nil) expect(project).not_to receive(:execute_hooks) diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore index 8e46d5a07f8..3826c85736f 100644 --- a/vendor/gitignore/Erlang.gitignore +++ b/vendor/gitignore/Erlang.gitignore @@ -4,7 +4,7 @@ deps *.beam *.plt erl_crash.dump -ebin +ebin/*.beam rel/example_project .concrete/DEV_MODE .rebar diff --git a/vendor/gitignore/Global/Ansible.gitignore b/vendor/gitignore/Global/Ansible.gitignore new file mode 100644 index 00000000000..a8b42eb6eed --- /dev/null +++ b/vendor/gitignore/Global/Ansible.gitignore @@ -0,0 +1 @@ +*.retry diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore index cc9586893b6..b56bf65d855 100644 --- a/vendor/gitignore/Global/Linux.gitignore +++ b/vendor/gitignore/Global/Linux.gitignore @@ -8,3 +8,6 @@ # Linux trash folder which might appear on any partition or disk .Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index cd0d5d1e2f4..397a0ed4acb 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -25,3 +25,6 @@ _testmain.go # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# external packages folder +vendor/ diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index bf7525f9912..bc7fc55724c 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -39,3 +39,6 @@ jspm_packages # Optional REPL history .node_repl_history + +# Output of 'npm pack' +*.tgz diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index 34f999df3e7..f620fad23eb 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -192,3 +192,6 @@ TSWLatexianTemp* # KBibTeX *~[0-9]* + +# auto folder when using emacs and auctex +/auto/* diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index d56f8b53288..1b86e7ec918 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -110,6 +110,10 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# Visual Studio code coverage results +*.coverage +*.coveragexml + # NCrunch _NCrunch_* .*crunch*.local.xml @@ -189,6 +193,7 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview +*.jfm *.pfx *.publishsettings node_modules/ @@ -258,3 +263,6 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc + +# Cake - Uncomment if you are using it +# tools/ diff --git a/vendor/gitlab-ci-yml/.gitlab-ci.yml b/vendor/gitlab-ci-yml/.gitlab-ci.yml new file mode 100644 index 00000000000..18b14554887 --- /dev/null +++ b/vendor/gitlab-ci-yml/.gitlab-ci.yml @@ -0,0 +1,4 @@ +image: ruby:2.3-alpine + +test: + script: ruby verify_templates.rb diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml new file mode 100644 index 00000000000..263c4c19999 --- /dev/null +++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml @@ -0,0 +1,34 @@ +# This template uses the java:8 docker image because there isn't any +# official Gradle image at this moment +# +# This is the Gradle build system for JVM applications +# https://gradle.org/ +# https://github.com/gradle/gradle +image: java:8 + +# Make the gradle wrapper executable. This essentially downloads a copy of +# Gradle to build the project with. +# https://docs.gradle.org/current/userguide/gradle_wrapper.html +# It is expected that any modern gradle project has a wrapper +before_script: + - chmod +x gradlew + +# We redirect the gradle user home using -g so that it caches the +# wrapper and dependencies. +# https://docs.gradle.org/current/userguide/gradle_command_line.html +# +# Unfortunately it also caches the build output so +# cleaning removes reminants of any cached builds. +# The assemble task actually builds the project. +# If it fails here, the tests can't run. +build: + stage: build + script: + - ./gradlew -g /cache/.gradle clean assemble + allow_failure: false + +# Use the generated build output to run the tests. +test: + stage: test + script: + - ./gradlew -g /cache./gradle check diff --git a/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml new file mode 100644 index 00000000000..140cb4635f3 --- /dev/null +++ b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml @@ -0,0 +1,54 @@ +# An example .gitlab-ci.yml file to test (and optionally report the coverage +# results of) your [Julia][1] packages. Please refer to the [documentation][2] +# for more information about package development in Julia. +# +# Here, it is assumed that your Julia package is named `MyPackage`. Change it to +# whatever name you have given to your package. +# +# [1]: http://julialang.org/ +# [2]: http://julia.readthedocs.org/ + +# Below is the template to run your tests in Julia +.test_template: &test_definition + # Uncomment below if you would like to run the tests on specific references + # only, such as the branches `master`, `development`, etc. + # only: + # - master + # - development + script: + # Let's run the tests. Substitute `coverage = false` below, if you do not + # want coverage results. + - /opt/julia/bin/julia -e 'Pkg.clone(pwd()); Pkg.test("MyPackage", + coverage = true)' + # Comment out below if you do not want coverage results. + - /opt/julia/bin/julia -e 'Pkg.add("Coverage"); cd(Pkg.dir("MyPackage")); + using Coverage; cl, tl = get_summary(process_folder()); + println("(", cl/tl*100, "%) covered")' + +# Name a test and select an appropriate image. +test:0.4.6: + image: julialang/julia:v0.4.6 + <<: *test_definition + +# Maybe you would like to test your package against the development branch: +test:0.5.0-dev: + image: julialang/julia:v0.5.0-dev + # ... allowing for failures, since we are testing against the development + # branch: + allow_failure: true + <<: *test_definition + +# REMARK: Do not forget to enable the coverage feature for your project, if you +# are using code coverage reporting above. This can be done by +# +# - Navigating to the `CI/CD Pipelines` settings of your project, +# - Copying and pasting the default `Simplecov` regex example provided, i.e., +# `\(\d+.\d+\%\) covered` in the `test coverage parsing` textfield. +# +# WARNING: This template is using the `julialang/julia` images from [Docker +# Hub][3]. One can use custom Julia images and/or the official ones found +# in the same place. However, care must be taken to correctly locate the binary +# file (`/opt/julia/bin/julia` above), which is usually given on the image's +# description page. +# +# [3]: http://hub.docker.com/ |