diff options
134 files changed, 1810 insertions, 547 deletions
diff --git a/.flayignore b/.flayignore index 9c9875d4f9e..f120de527bd 100644 --- a/.flayignore +++ b/.flayignore @@ -1 +1,2 @@ *.erb +lib/gitlab/sanitizers/svg/whitelist.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f51d506f64a..b167fc74996 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -206,10 +206,15 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 - bundle exec $CI_BUILD_NAME rubocop: *exec +rake haml_lint: *exec rake scss_lint: *exec rake brakeman: *exec -rake flog: *exec -rake flay: *exec +rake flog: + <<: *exec + allow_failure: yes +rake flay: + <<: *exec + allow_failure: yes license_finder: *exec rake downtime_check: *exec diff --git a/.haml-lint.yml b/.haml-lint.yml new file mode 100644 index 00000000000..da9a43d9c6d --- /dev/null +++ b/.haml-lint.yml @@ -0,0 +1,103 @@ +# Whether to ignore frontmatter at the beginning of HAML documents for +# frameworks such as Jekyll/Middleman +skip_frontmatter: false +exclude: + - 'vendor/**/*' + - 'spec/**/*' + +linters: + AltText: + enabled: false + + ClassAttributeWithStaticValue: + enabled: false + + ClassesBeforeIds: + enabled: false + + ConsecutiveComments: + enabled: false + + ConsecutiveSilentScripts: + enabled: false + max_consecutive: 2 + + EmptyObjectReference: + enabled: true + + EmptyScript: + enabled: true + + FinalNewline: + enabled: false + present: true + + HtmlAttributes: + enabled: false + + ImplicitDiv: + enabled: false + + LeadingCommentSpace: + enabled: false + + LineLength: + enabled: false + max: 80 + + MultilinePipe: + enabled: false + + MultilineScript: + enabled: true + + ObjectReferenceAttributes: + enabled: true + + RuboCop: + enabled: false + # These cops are incredibly noisy when it comes to HAML templates, so we + # ignore them. + ignored_cops: + - Lint/BlockAlignment + - Lint/EndAlignment + - Lint/Void + - Metrics/LineLength + - Style/AlignParameters + - Style/BlockNesting + - Style/ElseAlignment + - Style/FileName + - Style/FinalNewline + - Style/FrozenStringLiteralComment + - Style/IfUnlessModifier + - Style/IndentationWidth + - Style/Next + - Style/TrailingBlankLines + - Style/TrailingWhitespace + - Style/WhileUntilModifier + + RubyComments: + enabled: false + + SpaceBeforeScript: + enabled: false + + SpaceInsideHashAttributes: + enabled: false + style: space + + Indentation: + enabled: true + character: space # or tab + + TagName: + enabled: true + + TrailingWhitespace: + enabled: false + + UnnecessaryInterpolation: + enabled: false + + UnnecessaryStringOutput: + enabled: false diff --git a/.rubocop.yml b/.rubocop.yml index 5bd31ccf329..b054675d677 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -767,26 +767,33 @@ Rails/ScopeArgs: RSpec/AnyInstance: Enabled: false -# Check that the first argument to the top level describe is the tested class or -# module. +# Check for expectations where `be(...)` can replace `eql(...)`. +RSpec/BeEql: + Enabled: false + +# Check that the first argument to the top level describe is a constant. RSpec/DescribeClass: Enabled: false -# Use `described_class` for tested class / module. +# Checks that tests use `described_class`. +RSpec/DescribedClass: + Enabled: false + +# Checks that the second argument to `describe` specifies a method. RSpec/DescribeMethod: Enabled: false -# Checks that the second argument to top level describe is the tested method -# name. -RSpec/DescribedClass: +# Checks if an example group does not include any tests. +RSpec/EmptyExampleGroup: Enabled: false + CustomIncludeMethods: [] -# Checks for long example. +# Checks for long examples. RSpec/ExampleLength: Enabled: false Max: 5 -# Do not use should when describing your tests. +# Checks that example descriptions do not start with "should". RSpec/ExampleWording: Enabled: false CustomTransform: @@ -795,6 +802,10 @@ RSpec/ExampleWording: not: does not IgnoredWords: [] +# Checks for `expect(...)` calls containing literal values. +RSpec/ExpectActual: + Enabled: false + # Checks the file and folder naming of the spec file. RSpec/FilePath: Enabled: false @@ -806,19 +817,65 @@ RSpec/FilePath: RSpec/Focus: Enabled: true +# Checks the arguments passed to `before`, `around`, and `after`. +RSpec/HookArgument: + Enabled: false + EnforcedStyle: implicit + +# Check that a consistent implict expectation style is used. +# TODO (rspeicher): Available in rubocop-rspec 1.8.0 +# RSpec/ImplicitExpect: +# Enabled: true +# EnforcedStyle: is_expected + # Checks for the usage of instance variables. RSpec/InstanceVariable: Enabled: false -# Checks for multiple top-level describes. +# Checks for `subject` definitions that come after `let` definitions. +RSpec/LeadingSubject: + Enabled: false + +# Checks unreferenced `let!` calls being used for test setup. +RSpec/LetSetup: + Enabled: false + +# Check that chains of messages are not being stubbed. +RSpec/MessageChain: + Enabled: false + +# Checks for consistent message expectation style. +RSpec/MessageExpectation: + Enabled: false + EnforcedStyle: allow + +# Checks for multiple top level describes. RSpec/MultipleDescribes: Enabled: false -# Enforces the usage of the same method on all negative message expectations. +# Checks if examples contain too many `expect` calls. +RSpec/MultipleExpectations: + Enabled: false + Max: 1 + +# Checks for explicitly referenced test subjects. +RSpec/NamedSubject: + Enabled: false + +# Checks for nested example groups. +RSpec/NestedGroups: + Enabled: false + MaxNesting: 2 + +# Checks for consistent method usage for negating expectations. RSpec/NotToNot: EnforcedStyle: not_to Enabled: true +# Checks for stubbed test subjects. +RSpec/SubjectStub: + Enabled: false + # Prefer using verifying doubles over normal doubles. RSpec/VerifiedDoubles: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 20daf1619a7..87520c67dd5 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-07-13 12:36:08 -0600 using RuboCop version 0.41.2. +# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.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: 154 +# Offense count: 158 Lint/AmbiguousRegexpLiteral: Enabled: false -# Offense count: 43 +# Offense count: 41 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Enabled: false -# Offense count: 14 +# Offense count: 16 Lint/HandleExceptions: Enabled: false @@ -23,28 +23,28 @@ Lint/HandleExceptions: Lint/Loop: Enabled: false -# Offense count: 15 +# Offense count: 16 Lint/ShadowingOuterLocalVariable: Enabled: false -# Offense count: 3 +# Offense count: 6 # Cop supports --auto-correct. Lint/StringConversionInInterpolation: Enabled: false -# Offense count: 44 +# Offense count: 49 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Enabled: false -# Offense count: 129 +# Offense count: 144 # Cop supports --auto-correct. # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. Lint/UnusedMethodArgument: Enabled: false -# Offense count: 12 +# Offense count: 9 # Cop supports --auto-correct. Performance/PushSplat: Enabled: false @@ -59,51 +59,51 @@ Performance/RedundantBlockCall: Performance/RedundantMatch: Enabled: false -# Offense count: 24 +# Offense count: 27 # Cop supports --auto-correct. # Configuration parameters: MaxKeyValuePairs. Performance/RedundantMerge: Enabled: false -# Offense count: 60 +# Offense count: 61 Rails/OutputSafety: Enabled: false -# Offense count: 128 +# Offense count: 129 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: strict, flexible Rails/TimeZone: Enabled: false -# Offense count: 12 +# Offense count: 15 # Cop supports --auto-correct. # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/Validation: Enabled: false -# Offense count: 217 +# Offense count: 273 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation Style/AlignParameters: Enabled: false -# Offense count: 32 +# Offense count: 30 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: always, conditionals Style/AndOr: Enabled: false -# Offense count: 47 +# Offense count: 50 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Enabled: false -# Offense count: 258 +# Offense count: 289 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent @@ -126,14 +126,14 @@ Style/ColonMethodCall: Style/CommentAnnotation: Enabled: false -# Offense count: 34 +# Offense count: 33 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Enabled: false -# Offense count: 789 +# Offense count: 881 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: leading, trailing @@ -144,11 +144,12 @@ Style/DotPosition: Style/DoubleNegation: Enabled: false -# Offense count: 3 +# Offense count: 4 +# Cop supports --auto-correct. Style/EachWithObject: Enabled: false -# Offense count: 30 +# Offense count: 25 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: empty, nil, both @@ -160,7 +161,7 @@ Style/EmptyElse: Style/EmptyLiteral: Enabled: false -# Offense count: 123 +# Offense count: 135 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Style/ExtraSpacing: @@ -172,16 +173,16 @@ Style/ExtraSpacing: Style/FormatString: Enabled: false -# Offense count: 48 +# Offense count: 51 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false -# Offense count: 11 +# Offense count: 9 Style/IfInsideElse: Enabled: false -# Offense count: 177 +# Offense count: 174 # Cop supports --auto-correct. # Configuration parameters: MaxLineLength. Style/IfUnlessModifier: @@ -194,7 +195,7 @@ Style/IfUnlessModifier: Style/IndentArray: Enabled: false -# Offense count: 89 +# Offense count: 97 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces @@ -208,7 +209,7 @@ Style/IndentHash: Style/Lambda: Enabled: false -# Offense count: 6 +# Offense count: 5 # Cop supports --auto-correct. Style/LineEndConcatenation: Enabled: false @@ -218,17 +219,21 @@ Style/LineEndConcatenation: Style/MethodCallParentheses: Enabled: false -# Offense count: 62 +# Offense count: 8 +Style/MethodMissing: + Enabled: false + +# Offense count: 85 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false -# Offense count: 10 +# Offense count: 8 # Cop supports --auto-correct. Style/NestedParenthesizedCalls: Enabled: false -# Offense count: 12 +# Offense count: 13 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. # SupportedStyles: skip_modifier_ifs, always @@ -242,12 +247,19 @@ Style/Next: Style/NumericLiteralPrefix: Enabled: false +# Offense count: 64 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Enabled: false + # Offense count: 29 # Cop supports --auto-correct. Style/ParallelAssignment: Enabled: false -# Offense count: 208 +# Offense count: 264 # Cop supports --auto-correct. # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -265,7 +277,7 @@ Style/PercentQLiterals: Style/PerlBackrefs: Enabled: false -# Offense count: 32 +# Offense count: 35 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ @@ -273,7 +285,7 @@ Style/PerlBackrefs: Style/PredicateName: Enabled: false -# Offense count: 28 +# Offense count: 27 # Cop supports --auto-correct. Style/PreferredHashMethods: Enabled: false @@ -283,14 +295,14 @@ Style/PreferredHashMethods: Style/Proc: Enabled: false -# Offense count: 20 +# Offense count: 22 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: compact, exploded Style/RaiseArgs: Enabled: false -# Offense count: 3 +# Offense count: 4 # Cop supports --auto-correct. Style/RedundantBegin: Enabled: false @@ -300,29 +312,29 @@ Style/RedundantBegin: Style/RedundantException: Enabled: false -# Offense count: 23 +# Offense count: 24 # Cop supports --auto-correct. Style/RedundantFreeze: Enabled: false -# Offense count: 377 +# Offense count: 408 # Cop supports --auto-correct. Style/RedundantSelf: Enabled: false -# Offense count: 94 +# Offense count: 93 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Enabled: false -# Offense count: 17 +# Offense count: 18 # Cop supports --auto-correct. Style/RescueModifier: Enabled: false -# Offense count: 2 +# Offense count: 5 # Cop supports --auto-correct. Style/SelfAssignment: Enabled: false @@ -339,42 +351,42 @@ Style/SingleLineBlockParams: Style/SingleLineMethods: Enabled: false -# Offense count: 119 +# Offense count: 124 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: space, no_space Style/SpaceBeforeBlockBraces: Enabled: false -# Offense count: 11 +# Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Style/SpaceBeforeFirstArg: Enabled: false -# Offense count: 130 +# Offense count: 141 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space Style/SpaceInsideBlockBraces: Enabled: false -# Offense count: 98 +# Offense count: 96 # Cop supports --auto-correct. Style/SpaceInsideBrackets: Enabled: false -# Offense count: 60 +# Offense count: 62 # Cop supports --auto-correct. Style/SpaceInsideParens: Enabled: false -# Offense count: 5 +# Offense count: 7 # Cop supports --auto-correct. Style/SpaceInsidePercentLiteralDelimiters: Enabled: false -# Offense count: 36 +# Offense count: 40 # Cop supports --auto-correct. # Configuration parameters: SupportedStyles. # SupportedStyles: use_perl_names, use_english_names @@ -388,21 +400,28 @@ Style/SpecialGlobalVars: Style/StringLiteralsInInterpolation: Enabled: false -# Offense count: 24 +# Offense count: 32 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. # IgnoredMethods: respond_to, define_method Style/SymbolProc: Enabled: false -# Offense count: 23 +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses +Style/TernaryParentheses: + Enabled: false + +# Offense count: 24 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma Style/TrailingCommaInArguments: Enabled: false -# Offense count: 113 +# Offense count: 102 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma @@ -415,7 +434,7 @@ Style/TrailingCommaInLiteral: Style/TrailingUnderscoreVariable: Enabled: false -# Offense count: 90 +# Offense count: 76 # Cop supports --auto-correct. Style/TrailingWhitespace: Enabled: false @@ -427,12 +446,12 @@ Style/TrailingWhitespace: Style/TrivialAccessors: Enabled: false -# Offense count: 3 +# Offense count: 2 # Cop supports --auto-correct. Style/UnlessElse: Enabled: false -# Offense count: 13 +# Offense count: 14 # Cop supports --auto-correct. Style/UnneededInterpolation: Enabled: false diff --git a/CHANGELOG b/CHANGELOG index 7f6d6dba1c0..54a5f32932d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,12 +7,15 @@ v 8.12.0 (unreleased) - Cleanup misalignments in Issue list view !6206 - 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 - Filter tags by name !6121 + - Update gitlab shell secret file also when it is empty. !3774 (glensc) - Give project selection dropdowns responsive width, make non-wrapping. - Make push events have equal vertical spacing. - Add two-factor recovery endpoint to internal API !5510 - Pass the "Remember me" value to the U2F authentication form - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) + - Move pushes_since_gc from the database to Redis - Add font color contrast to external label in admin area (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps) - Instructions for enabling Git packfile bitmaps !6104 @@ -28,6 +31,7 @@ v 8.12.0 (unreleased) - 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) + - Added go to issue boards keyboard shortcut - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) - Fix blame table layout width @@ -35,6 +39,7 @@ v 8.12.0 (unreleased) - Request only the LDAP attributes we need !6187 - Center build stage columns in pipeline overview (ClemMakesApps) - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps) + - Fix bug stopping issue description being scrollable after selecting issue template - Remove suggested colors hover underline (ClemMakesApps) - Shorten task status phrase (ClemMakesApps) - Fix project visibility level fields on settings @@ -73,13 +78,15 @@ v 8.12.0 (unreleased) - Add last commit time to repo view (ClemMakesApps) - Fix accessibility and visibility of project list dropdown button !6140 - Fix missing flash messages on service edit page (airatshigapov) - - Added project specific enable/disable setting for LFS !5997 + - Added project-specific enable/disable setting for LFS !5997 + - Added group-specific enable/disable setting for LFS !6164 - Don't expose a user's token in the `/api/v3/user` API (!6047) - Remove redundant js-timeago-pending from user activity log (ClemMakesApps) - Ability to manage project issues, snippets, wiki, merge requests and builds access level - Remove inconsistent font weight for sidebar's labels (ClemMakesApps) - Align add button on repository view (ClemMakesApps) - Fix contributions calendar month label truncation (ClemMakesApps) + - Import release note descriptions from GitHub (EspadaV8) - Added tests for diff notes - Add pipeline events to Slack integration !5525 - Add a button to download latest successful artifacts for branches and tags !5142 @@ -98,6 +105,7 @@ v 8.12.0 (unreleased) - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps) - Update merge_requests.md with a simpler way to check out a merge request. !5944 - Fix button missing type (ClemMakesApps) + - Gitlab::Checks is now instrumented - Move to project dropdown with infinite scroll for better performance - Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz) - Load branches asynchronously in Cherry Pick and Revert dialogs. @@ -116,6 +124,7 @@ v 8.12.0 (unreleased) - Avoid conflict with admin labels when importing GitHub labels - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 - Fix repository page ui issues + - Avoid protected branches checks when verifying access without branch name - Add information about user and manual build start to runner as variables !6201 (Sergey Gnuskov) - Fixed invisible scroll controls on build page on iPhone - Fix error on raw build trace download for old builds stored in database !4822 @@ -126,9 +135,22 @@ v 8.12.0 (unreleased) - Allow bulk update merge requests from merge requests index page - Add notification_settings API calls !5632 (mahcsig) - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska) - -v 8.11.6 (unreleased) - - Fix an error where we were unable to create a CommitStatus for running state + - Fix URLs with anchors in wiki !6300 (houqp) + - Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska) + - Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225 + - Fix Gitlab::Popen.popen thread-safety issue + - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska) + - Clean environment variables when running git hooks + +v 8.11.6 + - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 + - Make merge conflict file size limit 200 KB, to match the docs. !6052 + - Fix an error where we were unable to create a CommitStatus for running state. !6107 + - Optimize discussion notes resolving and unresolving. !6141 + - Fix GitLab import button. !6167 + - Restore SSH Key title auto-population behavior. !6186 + - Fix DB schema to match latest migration. !6256 + - Exclude some pending or inactivated rows in Member scopes. v 8.11.5 - Optimize branch lookups and force a repository reload for Repository#find_branch. !6087 @@ -333,6 +355,13 @@ v 8.11.0 - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done +v 8.10.9 + - Exclude some pending or inactivated rows in Member scopes + +v 8.10.8 + - Fix information disclosure in issue boards. + - Fix privilege escalation in project import. + v 8.10.7 - Upgrade Hamlit to 2.6.1. !5873 - Upgrade Doorkeeper to 4.2.0. !5881 @@ -558,6 +587,9 @@ v 8.10.0 - Fix migration corrupting import data for old version upgrades - Show tooltip on GitLab export link in new project page +v 8.9.9 + - Exclude some pending or inactivated rows in Member scopes + v 8.9.8 - Upgrade Doorkeeper to 4.2.0. !5881 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c77dcd96a7d..549918284c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,19 +91,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs]. ## Implement design & UI elements -### Design reference - -The GitLab design reference can be found in the [gitlab-design] project. -The designs are made using Antetype (`.atype` files). You can use the -[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design -(the PNG is 1:1). - -The current designs can be found in the [`gitlab8.atype` file]. - -### UI development kit - -Implemented UI elements can also be found at https://gitlab.com/help/ui. Please -note that this page isn't comprehensive at this time. +Please see the [UI Guide for building GitLab]. ## Issue tracker @@ -489,7 +477,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" -[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design -[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 -[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ +[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md [license-finder-doc]: doc/development/licensing.md diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 18091983f59..1545d966571 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.4.0 +3.5.0 @@ -26,7 +26,7 @@ gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' -gem 'omniauth-facebook', '~> 3.0.0' +gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.4.1' @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.6.3' +gem 'gitlab_git', '~> 10.6.6' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -295,9 +295,10 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.41.2', require: false - gem 'rubocop-rspec', '~> 1.5.0', require: false + gem 'rubocop', '~> 0.42.0', require: false + gem 'rubocop-rspec', '~> 1.7.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 diff --git a/Gemfile.lock b/Gemfile.lock index c421713f6a1..1edb218373d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,7 +279,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.6.3) + gitlab_git (10.6.6) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -322,11 +322,18 @@ GEM grape-entity (0.4.8) activesupport multi_json (>= 1.3.2) + haml (4.0.7) + tilt + haml_lint (0.18.2) + haml (~> 4.0) + rake (>= 10, < 12) + rubocop (>= 0.36.0) + sysexits (~> 1.1) hamlit (2.6.1) temple (~> 0.7.6) thor tilt - hashie (3.4.3) + hashie (3.4.4) health_check (2.1.0) rails (>= 4.0) hipchat (1.5.2) @@ -394,7 +401,7 @@ GEM mime-types (>= 1.16, < 4) mail_room (0.8.0) method_source (0.8.2) - mime-types (2.99.2) + mime-types (2.99.3) mimemagic (0.3.0) mini_portile2 (2.1.0) minitest (5.7.0) @@ -437,7 +444,7 @@ GEM addressable (~> 2.3) nokogiri (~> 1.6.6) omniauth (~> 1.2) - omniauth-facebook (3.0.0) + omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) omniauth-github (1.1.2) omniauth (~> 1.0) @@ -612,14 +619,14 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.5.0) - rubocop (0.41.2) + rubocop (0.42.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.5.0) - rubocop (>= 0.40.0) + rubocop-rspec (1.7.0) + rubocop (>= 0.42.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-prof (0.15.9) @@ -723,6 +730,7 @@ GEM stringex (2.5.2) sys-filesystem (1.1.6) ffi + sysexits (1.2.0) systemu (2.6.5) task_list (1.0.2) html-pipeline @@ -754,7 +762,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.1.0) + unicode-display_width (1.1.1) unicorn (4.9.0) kgio (~> 2.6) rack @@ -858,7 +866,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.6.3) + gitlab_git (~> 10.6.6) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) @@ -866,6 +874,7 @@ DEPENDENCIES gon (~> 6.1.0) grape (~> 0.15.0) grape-entity (~> 0.4.2) + haml_lint (~> 0.18.2) hamlit (~> 2.6.1) health_check (~> 2.1.0) hipchat (~> 1.5.0) @@ -900,7 +909,7 @@ DEPENDENCIES omniauth-azure-oauth2 (~> 0.0.6) omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) - omniauth-facebook (~> 3.0.0) + omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) omniauth-google-oauth2 (~> 0.4.1) @@ -935,8 +944,8 @@ DEPENDENCIES rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) - rubocop (~> 0.41.2) - rubocop-rspec (~> 1.5.0) + rubocop (~> 0.42.0) + rubocop-rspec (~> 1.7.0) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.15.9) sanitize (~> 2.0) diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index b18b6962382..95352164d76 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -13,6 +13,9 @@ this.buildDropdown(); this.bindEvents(); this.onFilenameUpdate(); + + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); } TemplateSelector.prototype.buildDropdown = function() { @@ -72,6 +75,10 @@ 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() { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index bea141bae51..087f27d9f4c 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -352,7 +352,13 @@ if (self.options.clicked) { self.options.clicked(selected, $el, e); } - return $el.trigger('blur'); + + // Update label right after all modifications in dropdown has been done + if (self.options.toggleLabel) { + self.updateLabel(selected, $el, self); + } + + $el.trigger('blur'); }); } } @@ -529,7 +535,7 @@ } else { if (!selected) { value = this.options.id ? this.options.id(data) : data.id; - fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName; + fieldName = this.options.fieldName; field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); if (field.length) { @@ -589,6 +595,7 @@ GitLabDropdown.prototype.rowClicked = function(el) { var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { groupName = el.data('group'); @@ -600,7 +607,6 @@ selectedObject = this.renderedData[selectedIndex]; } } - fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; if (isInput) { field = $(this.el); @@ -644,11 +650,6 @@ } } - // Update label right after input has been added - if (this.options.toggleLabel) { - this.updateLabel(selectedObject, el, this); - } - return selectedObject; }; @@ -659,9 +660,6 @@ if (this.options.inputId != null) { $input.attr('id', this.options.inputId); } - if (selectedObject && selectedObject.type) { - $input.attr('data-type', selectedObject.type); - } return this.dropdown.before($input); }; diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 469e25482bb..b04159420d1 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -34,6 +34,9 @@ Mousetrap.bind('g i', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); }); + Mousetrap.bind('g l', function() { + ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards'); + }); Mousetrap.bind('g m', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'); }); diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 965fcc06518..46af18580d5 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -162,6 +162,10 @@ ul.content-list { margin-right: 0; } } + + .no-comments { + opacity: 0.5; + } } // When dragging a list item diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 367c7d01944..76b93b23b95 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -79,10 +79,6 @@ padding-left: 15px !important; } - .issue-info, .merge-request-info { - display: none; - } - .nav-links, .nav-links { li a { font-size: 14px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 63845e6b9ba..41079b6eeb5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -206,7 +206,7 @@ padding-top: 0; .block { - width: $sidebar_collapsed_width - 1px; + width: $sidebar_collapsed_width - 2px; margin-left: -19px; padding: 15px 0 0; border-bottom: none; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 7a26b7ad497..3ac34cbc829 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -10,10 +10,6 @@ .issue-labels { display: inline-block; } - - .issue-no-comments { - opacity: 0.5; - } } } @@ -37,6 +33,15 @@ form.edit-issue { margin: 0; } +ul.related-merge-requests > li { + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + .merge-request-id { + flex-shrink: 0; + } +} + .merge-requests-title, .related-branches-title { font-size: 16px; font-weight: 600; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2a44b95de64..96c06086867 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -231,10 +231,6 @@ .merge-request-labels { display: inline-block; } - - .merge-request-no-comments { - opacity: 0.5; - } } .merge-request-angle { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 2d66ab25da6..1b4d12d3053 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -147,14 +147,37 @@ } .stage-cell { - text-align: center; + font-size: 0; svg { height: 18px; width: 18px; + position: relative; + z-index: 2; vertical-align: middle; overflow: visible; } + + .stage-container { + display: inline-block; + position: relative; + margin-right: 6px; + + .tooltip { + white-space: nowrap; + } + + &:not(:last-child) { + &::after { + content: ''; + width: 8px; + position: absolute;; + right: -7px; + bottom: 8px; + border-bottom: 2px solid $border-color; + } + } + } } .duration, @@ -318,9 +341,17 @@ .build-content { width: 130px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + + .ci-status-text { + width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + display: inline-block; + position: relative; + top: -1px; + } a { color: $layout-link-gray; @@ -331,13 +362,74 @@ text-decoration: underline; } } + } + + .dropdown-menu-toggle { + border: none; + width: auto; + padding: 0; + color: $layout-link-gray; + + .ci-status-text { + width: 80px; + } + } + + .grouped-pipeline-dropdown { + padding: 8px 0; + width: 200px; + left: auto; + right: -214px; + top: -9px; + + a:hover { + .ci-status-text { + text-decoration: none; + } + } + .ci-status-text { + width: 145px; + } + + .arrow { + &:before, + &:after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 18px; + } + + &:before { + left: -5px; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: $border-color; + } + + &:after { + left: -4px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: $white-light; + } + } + } + + .badge { + background-color: $gray-dark; + color: $layout-link-gray; + font-weight: normal; } } svg { - position: relative; - top: 2px; + vertical-align: middle; margin-right: 5px; } @@ -442,7 +534,7 @@ width: 21px; height: 25px; position: absolute; - top: -28.5px; + top: -29px; border-top: 2px solid $border-color; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3e6e50375f6..db46d8072ce 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -334,6 +334,10 @@ a.deploy-project-label { a { color: $gl-dark-link-color; } + + .dropdown-menu { + width: 240px; + } } .last-push-widget { diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 2aa939b7dc3..5270aea4e79 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -2,20 +2,6 @@ padding: 2px; } -.snippet-holder { - margin-bottom: -$gl-padding; - - .file-holder { - border-top: 0; - } - - .file-actions { - .btn-clipboard { - @extend .btn; - } - } -} - .markdown-snippet-copy { position: fixed; top: -10px; @@ -24,29 +10,18 @@ max-width: 0; } -.file-holder.snippet-file-content { - padding-bottom: $gl-padding; - border-bottom: 1px solid $border-color; - - .file-title { - padding-top: $gl-padding; - padding-bottom: $gl-padding; - } - - .file-actions { - top: 12px; - } - - .file-content { - border-left: 1px solid $border-color; - border-right: 1px solid $border-color; - border-bottom: 1px solid $border-color; +.snippet-file-content { + border-radius: 3px; + .btn-clipboard { + @extend .btn; } } .snippet-title { font-size: 24px; - font-weight: normal; + font-weight: 600; + padding: $gl-padding; + padding-left: 0; } .snippet-actions { diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index cdfa8d91a28..aed77d0358a 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -60,6 +60,14 @@ class Admin::GroupsController < Admin::ApplicationController end def group_params - params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled) + params.require(:group).permit( + :avatar, + :description, + :lfs_enabled, + :name, + :path, + :request_access_enabled, + :visibility_level + ) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index cb82d62616c..b83c3a872cf 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -121,7 +121,17 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled) + params.require(:group).permit( + :avatar, + :description, + :lfs_enabled, + :name, + :path, + :public, + :request_access_enabled, + :share_with_group_lock, + :visibility_level + ) end def load_events diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 77934ff9962..f13fb1df373 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -74,7 +74,7 @@ class Projects::BuildsController < Projects::ApplicationController def erase @build.erase(erased_by: current_user) redirect_to namespace_project_build_path(project.namespace, project, @build), - notice: "Build has been sucessfully erased!" + notice: "Build has been successfully erased!" end def raw diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 8895cb955bd..aa8645ba8cc 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -428,17 +428,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def validates_merge_request - # If source project was removed (Ex. mr from fork to origin) - return invalid_mr unless @merge_request.source_project - # Show git not found page # if there is no saved commits between source & target branch if @merge_request.commits.blank? # and if target branch doesn't exist return invalid_mr unless @merge_request.target_branch_exists? - - # or if source branch doesn't exist - return invalid_mr unless @merge_request.source_branch_exists? end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index b9211e88473..ab880ed6de0 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -23,4 +23,29 @@ module GroupsHelper full_title end end + + def projects_lfs_status(group) + lfs_status = + if group.lfs_enabled? + group.projects.select(&:lfs_enabled?).size + else + group.projects.reject(&:lfs_enabled?).size + end + + size = group.projects.size + + if lfs_status == size + 'for all projects' + else + "for #{lfs_status} out of #{pluralize(size, 'project')}" + end + end + + def group_lfs_status(group) + status = group.lfs_enabled? ? 'enabled' : 'disabled' + + content_tag(:span, class: "lfs-#{status}") do + "#{status.humanize} #{projects_lfs_status(group)}" + end + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 16a8e52a4ca..56477733ea2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -27,7 +27,7 @@ module ProjectsHelper author_html = "" # Build avatar image tag - author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar] + author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar] # Build name span tag if opts[:by_username] diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index e523c46e879..8a7446b7cc7 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -30,6 +30,37 @@ module SearchHelper "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\"" end + def parse_search_result(result) + ref = nil + filename = nil + basename = nil + startline = 0 + + result.each_line.each_with_index do |line, index| + if line =~ /^.*:.*:\d+:/ + ref, filename, startline = line.split(':') + startline = startline.to_i - index + extname = Regexp.escape(File.extname(filename)) + basename = filename.sub(/#{extname}$/, '') + break + end + end + + data = "" + + result.each_line do |line| + data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '') + end + + OpenStruct.new( + filename: filename, + basename: basename, + ref: ref, + startline: startline, + data: data + ) + end + private # Autocomplete results for various settings pages diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 0a5a8eb5aee..7e33a562077 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,10 +1,10 @@ module SnippetsHelper - def reliable_snippet_path(snippet) + def reliable_snippet_path(snippet, opts = nil) if snippet.project_id? namespace_project_snippet_path(snippet.project.namespace, - snippet.project, snippet) + snippet.project, snippet, opts) else - snippet_path(snippet) + snippet_path(snippet, opts) end end diff --git a/app/models/blob.rb b/app/models/blob.rb index 12cc5aaafba..ab92e820335 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -22,6 +22,18 @@ class Blob < SimpleDelegator new(blob) end + # Returns the data of the blob. + # + # If the blob is a text based blob the content is converted to UTF-8 and any + # invalid byte sequences are replaced. + def data + if binary? + super + else + @data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace) + end + end + def no_highlighting? size && size > 1.megabyte end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index c9c47ec7419..6959223aed9 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,7 +1,7 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model - + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id validates_uniqueness_of :key, scope: :gl_project_id @@ -11,7 +11,9 @@ module Ci format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } - attr_encrypted :value, + scope :order_key_asc, -> { reorder(key: :asc) } + + attr_encrypted :value, mode: :per_attribute_iv_and_salt, insecure_mode: true, key: Gitlab::Application.secrets.db_key_base, diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4a628924499..c85561291c8 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -69,17 +69,15 @@ class CommitStatus < ActiveRecord::Base commit_status.update_attributes finished_at: Time.now end - # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed - around_transition any => [:success, :failed, :canceled] do |commit_status, block| - block.call - - commit_status.pipeline.try(:process!) - end - after_transition do |commit_status, transition| commit_status.pipeline.try(:build_updated) unless transition.loopback? end + after_transition any => [:success, :failed, :canceled] do |commit_status| + commit_status.pipeline.try(:process!) + true + end + after_transition [:created, :pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) end @@ -95,6 +93,10 @@ class CommitStatus < ActiveRecord::Base pipeline.before_sha || Gitlab::Git::BLANK_SHA end + def group_name + name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip + end + def self.stages # We group by stage name, but order stages by theirs' index unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') @@ -113,6 +115,10 @@ class CommitStatus < ActiveRecord::Base allow_failure? && (failed? || canceled?) end + def playable? + false + end + def duration calculate_duration end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index f7b8352405c..d658552f695 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -8,8 +8,9 @@ module HasStatus class_methods do def status_sql - scope = all.relevant + scope = all 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' @@ -19,12 +20,12 @@ module HasStatus skipped = scope.skipped.select('count(*)').to_sql deduce_status = "(CASE - WHEN (#{builds})=0 THEN NULL + WHEN (#{builds})=(#{created}) THEN NULL WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' - WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending' + WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled' - WHEN (#{running})+(#{pending})>0 THEN 'running' + WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' ELSE 'failed' END)" diff --git a/app/models/group.rb b/app/models/group.rb index c48869ae465..aefb94b2ada 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -95,6 +95,13 @@ class Group < Namespace end end + def lfs_enabled? + return false unless Gitlab.config.lfs.enabled + return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil? + + 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( diff --git a/app/models/member.rb b/app/models/member.rb index 64e0d33fb20..69406379948 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -28,17 +28,34 @@ class Member < ActiveRecord::Base allow_nil: true } + # This scope encapsulates (most of) the conditions a row in the member table + # must satisfy if it is a valid permission. Of particular note: + # + # * Access requests must be excluded + # * Blocked users must be excluded + # * Invitations take effect immediately + # * expires_at is not implemented. A background worker purges expired rows + scope :active, -> do + is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) + user_is_active = User.arel_table[:state].eq(:active) + + includes(:user).references(:users) + .where(is_external_invite.or(user_is_active)) + .where(requested_at: nil) + end + scope :invite, -> { where.not(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) } scope :request, -> { where.not(requested_at: nil) } - scope :has_access, -> { where('access_level > 0') } - - scope :guests, -> { where(access_level: GUEST) } - scope :reporters, -> { where(access_level: REPORTER) } - scope :developers, -> { where(access_level: DEVELOPER) } - scope :masters, -> { where(access_level: MASTER) } - scope :owners, -> { where(access_level: OWNER) } - scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) } + + scope :has_access, -> { active.where('access_level > 0') } + + scope :guests, -> { active.where(access_level: GUEST) } + scope :reporters, -> { active.where(access_level: REPORTER) } + scope :developers, -> { active.where(access_level: DEVELOPER) } + scope :masters, -> { active.where(access_level: MASTER) } + scope :owners, -> { active.where(access_level: OWNER) } + scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b0b1313f94a..f7d1253d957 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -316,6 +316,10 @@ class MergeRequest < ActiveRecord::Base closed? && forked_source_project_missing? end + def closed_without_source_project? + closed? && !source_project + end + def forked_source_project_missing? return false unless for_fork? return true unless source_project @@ -323,6 +327,12 @@ class MergeRequest < ActiveRecord::Base !source_project.forked_from?(target_project) end + def reopenable? + return false if closed_without_fork? || closed_without_source_project? || merged? + + closed? + end + def ensure_merge_request_diff merge_request_diff || create_merge_request_diff end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7c29d27ce97..919b3b1f095 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -141,6 +141,11 @@ class Namespace < ActiveRecord::Base projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) end + def lfs_enabled? + # User namespace will always default to the global setting + Gitlab.config.lfs.enabled + end + private def repository_storage_paths diff --git a/app/models/project.rb b/app/models/project.rb index 4017cabe9f0..8b5a6f167bd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -393,10 +393,9 @@ class Project < ActiveRecord::Base end def lfs_enabled? - return false unless Gitlab.config.lfs.enabled - return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil? + return namespace.lfs_enabled? if self[:lfs_enabled].nil? - self[:lfs_enabled] + self[:lfs_enabled] && Gitlab.config.lfs.enabled end def repository_storage_path @@ -1288,8 +1287,24 @@ class Project < ActiveRecord::Base end end + def pushes_since_gc + Gitlab::Redis.with { |redis| redis.get(pushes_since_gc_redis_key).to_i } + end + + def increment_pushes_since_gc + Gitlab::Redis.with { |redis| redis.incr(pushes_since_gc_redis_key) } + end + + def reset_pushes_since_gc + Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } + end + private + def pushes_since_gc_redis_key + "projects/#{id}/pushes_since_gc" + end + # Prevents the creation of project_feature record for every project def setup_project_feature build_project_feature unless project_feature diff --git a/app/models/repository.rb b/app/models/repository.rb index 3c354c25c6f..c69e5a22a69 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -990,37 +990,6 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end - def parse_search_result(result) - ref = nil - filename = nil - basename = nil - startline = 0 - - result.each_line.each_with_index do |line, index| - if line =~ /^.*:.*:\d+:/ - ref, filename, startline = line.split(':') - startline = startline.to_i - index - extname = Regexp.escape(File.extname(filename)) - basename = filename.sub(/#{extname}$/, '') - break - end - end - - data = "" - - result.each_line do |line| - data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '') - end - - OpenStruct.new( - filename: filename, - basename: basename, - ref: ref, - startline: startline, - data: data - ) - end - def fetch_ref(source_path, source_ref, target_ref) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) @@ -1048,7 +1017,7 @@ class Repository GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do update_ref!(ref, newrev, oldrev) - + if was_empty || !target_branch # If repo was empty expire cache after_create if was_empty diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index ed73d8cb8c2..1c82599c579 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -16,11 +16,29 @@ module Commits error(ex.message) end + private + def commit raise NotImplementedError end - private + def commit_change(action) + raise NotImplementedError unless repository.respond_to?(action) + + into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch + tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch) + + if tree_id + create_target_branch(into) if @create_merge_request + + repository.public_send(action, current_user, @commit, into, tree_id) + success + else + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. + It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." + raise ChangeError, error_msg + end + end def check_push_permissions allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch) diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb index f9a4efa7182..605cca36f9c 100644 --- a/app/services/commits/cherry_pick_service.rb +++ b/app/services/commits/cherry_pick_service.rb @@ -1,19 +1,7 @@ module Commits class CherryPickService < ChangeService def commit - cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch - cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch) - - if cherry_pick_tree_id - create_target_branch(cherry_pick_into) if @create_merge_request - - repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id) - success - else - error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically. - It may have already been cherry-picked, or a more recent commit may have updated some of its content." - raise ChangeError, error_msg - end + commit_change(:cherry_pick) end end end diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb index c7de9f6f35e..addd55cb32f 100644 --- a/app/services/commits/revert_service.rb +++ b/app/services/commits/revert_service.rb @@ -1,19 +1,7 @@ module Commits class RevertService < ChangeService def commit - revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch - revert_tree_id = repository.check_revert_content(@commit, @target_branch) - - if revert_tree_id - create_target_branch(revert_into) if @create_merge_request - - repository.revert(current_user, @commit, revert_into, revert_tree_id) - success - else - error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically. - It may have already been reverted, or a more recent commit may have updated some of its content." - raise ChangeError, error_msg - end + commit_change(:revert) end end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 8a53f65aec1..a08c6fcd94b 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -27,6 +27,8 @@ module Projects # Git data (e.g. a list of branch names). flush_caches(project, wiki_path) + Projects::UnlinkForkService.new(project, current_user).execute + Project.transaction do project.destroy! diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 29b3981f49f..c3dfc8cfbe8 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -30,10 +30,8 @@ module Projects end def increment! - if Gitlab::ExclusiveLease.new("project_housekeeping:increment!:#{@project.id}", timeout: 60).try_obtain - Gitlab::Metrics.measure(:increment_pushes_since_gc) do - update_pushes_since_gc(@project.pushes_since_gc + 1) - end + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + @project.increment_pushes_since_gc end end @@ -43,14 +41,10 @@ module Projects GitGarbageCollectWorker.perform_async(@project.id) ensure Gitlab::Metrics.measure(:reset_pushes_since_gc) do - update_pushes_since_gc(0) + @project.reset_pushes_since_gc end end - def update_pushes_since_gc(new_value) - @project.update_column(:pushes_since_gc, new_value) - end - def try_obtain_lease Gitlab::Metrics.measure(:obtain_housekeeping_lease) do lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 5f7fdfdb011..817910f7ddf 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -13,6 +13,8 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f + = render 'groups/group_lfs_settings', f: f + - if @group.new_record? .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index bb374694400..0188ed448ce 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -37,6 +37,12 @@ %strong = @group.created_at.to_s(:medium) + %li + %span.light Group Git LFS status: + %strong + = group_lfs_status(@group) + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + .panel.panel-default .panel-heading %h3.panel-title diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml new file mode 100644 index 00000000000..af57065f0fc --- /dev/null +++ b/app/views/groups/_group_lfs_settings.html.haml @@ -0,0 +1,11 @@ +- if current_user.admin? + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled, checked: @group.lfs_enabled? + %strong + Allow projects within this group to use Git LFS + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + %br/ + %span.descr This setting can be overridden in each project.
\ No newline at end of file diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index decb89b2fd6..c766370d5a0 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -25,6 +25,8 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f + = render 'group_lfs_settings', f: f + .form-group %hr = f.label :share_with_group_lock, class: 'control-label' do diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 16c16cec137..65842a0479b 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -165,6 +165,12 @@ %tr %td.shortcut .key g + .key l + %td + Go to issue boards + %tr + %td.shortcut + .key g .key m %td Go to merge requests diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index f7012595a5a..8e4937b7aa0 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -113,3 +113,7 @@ %li.hidden = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do Commits + + -# Shortcut to issue boards + %li.hidden + = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index a42b3b8eb38..93187873501 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,4 +1,5 @@ - page_title "SSH Keys" += render 'profiles/head' .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml index 249680bcab6..de1337a2a24 100644 --- a/app/views/profiles/update_username.js.haml +++ b/app/views/profiles/update_username.js.haml @@ -1,6 +1,6 @@ - if @user.valid? :plain - new Flash("Username sucessfully changed", "notice") + new Flash("Username successfully changed", "notice") - else :plain new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert") diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 36fb0300aeb..547bc0c9c19 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,15 +1,12 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) -%li.build{class: ("playable" if is_playable)} - .curve - .build-content - - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do - = render_status_with_link('build', 'play') - %span.ci-status-text= subject.name - - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do - = render_status_with_link('build', subject.status) - %span.ci-status-text= subject.name - - else - = render_status_with_link('build', subject.status) - = ci_icon_for_status(subject.status) +- if is_playable + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do + = render_status_with_link('build', 'play') + .ci-status-text= subject.name +- elsif can?(current_user, :read_build, @project) + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do + = render_status_with_link('build', subject.status) + .ci-status-text= subject.name +- else + = render_status_with_link('build', subject.status) + = ci_icon_for_status(subject.status) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index bb9493f5158..6391c67021b 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -36,16 +36,14 @@ - stages_status = pipeline.statuses.relevant.latest.stages_status - - stages.each do |stage| - %td.stage-cell + %td.stage-cell + - stages.each do |stage| - status = stages_status[stage] - tooltip = "#{stage.titleize}: #{status || 'not found'}" - if status - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - = ci_icon_for_status(status) - - else - .light.has-tooltip{ title: tooltip } - \- + .stage-container + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = ci_icon_for_status(status) %td - if pipeline.duration diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 20a85148ab5..9258f4b3c25 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -39,8 +39,7 @@ = stage.titleize .builds-container %ul - - statuses.each do |status| - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render "projects/commit/pipeline_stage", statuses: statuses - if pipeline.yaml_errors.present? diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml new file mode 100644 index 00000000000..23c5c51fbc2 --- /dev/null +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -0,0 +1,14 @@ +- status_groups = statuses.group_by(&:group_name) +- status_groups.each do |group_name, grouped_statuses| + - if grouped_statuses.one? + - status = grouped_statuses.first + - is_playable = status.playable? && can?(current_user, :update_build, @project) + %li.build{ class: ("playable" if is_playable) } + .curve + .build-content + = render "projects/#{status.to_partial_path}_pipeline", subject: status + - else + %li.build + .curve + .build-content + = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml new file mode 100644 index 00000000000..4e7a6f1af08 --- /dev/null +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -0,0 +1,11 @@ +- group_status = CommitStatus.where(id: subject).status += render_status_with_link('build', group_status) +.dropdown.inline + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %span.ci-status-text + = name + %span.badge= subject.size + %ul.dropdown-menu.grouped-pipeline-dropdown + .arrow + - subject.each do |status| + = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f6d751a343e..a04d53e02bf 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,15 +84,14 @@ = project_feature_access_select(:snippets_access_level) - if Gitlab.config.lfs.enabled && current_user.admin? - .form-group - .checkbox - = f.label :lfs_enabled do - = f.check_box :lfs_enabled, checked: @project.lfs_enabled? - %strong LFS - %br - %span.descr - Git Large File Storage - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + .row + .col-md-9 + = f.label :lfs_enabled, 'LFS', class: 'label-light' + %span.help-block + Git Large File Storage + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + .col-md-3 + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control' - if Gitlab.config.registry.enabled .form-group diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 576d0bec51b..409f4701e4b 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,10 +1,7 @@ -%li.build - .curve - .build-content - - if subject.target_url - - link_to subject.target_url do - = render_status_with_link('commit status', subject.status) - %span.ci-status-text= subject.name - - else - = render_status_with_link('commit status', subject.status) - %span.ci-status-text= subject.name +- if subject.target_url + = link_to subject.target_url do + = render_status_with_link('commit status', subject.status) + %span.ci-status-text= subject.name +- else + = render_status_with_link('commit status', subject.status) + %span.ci-status-text= subject.name diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 851d4c06990..8b1a8a8a2d9 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -29,7 +29,7 @@ - note_count = issue.notes.user.count %li - = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do + = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') = note_count diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index d8075371853..31d3ec23276 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -1,7 +1,7 @@ - if @merge_requests.any? %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') - %ul.unstyled-list + %ul.unstyled-list.related-merge-requests - has_any_ci = @merge_requests.any?(&:pipeline) - @merge_requests.each do |merge_request| %li diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index a8eeab3e55e..44683c8bcdb 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -1,7 +1,7 @@ - if @related_branches.any? %h2.related-branches-title = pluralize(@related_branches.count, 'Related Branch') - %ul.unstyled-list + %ul.unstyled-list.related-merge-requests - @related_branches.each do |branch| %li - target = @project.repository.find_branch(branch).target diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index d070979bcfe..3900b4f6f17 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -2,7 +2,7 @@ - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"} - - if @merge_request.closed? + - 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}" } } diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 31f8d0aeb5b..68fb7d5a414 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -41,7 +41,7 @@ - note_count = merge_request.mr_and_commit_notes.user.count %li - = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do + = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') = note_count diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 4b4d418e8ec..d03ff9ec7e8 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -29,17 +29,19 @@ %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - .normal - %span Request to merge - %span.label-branch= source_branch_with_namespace(@merge_request) - %span into - %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - - if @merge_request.open? && @merge_request.diverged_from_target_branch? - %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) + - unless @merge_request.closed_without_fork? + .normal + %span Request to merge + %span.label-branch= source_branch_with_namespace(@merge_request) + %span into + %span.label-branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + - if @merge_request.open? && @merge_request.diverged_from_target_branch? + %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) - = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + - unless @merge_request.closed_without_source_project? + = render "projects/merge_requests/show/how_to_merge" + = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) .light.prepend-top-default.append-bottom-default @@ -53,10 +55,11 @@ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count + - unless @merge_request.closed_without_source_project? + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count - if @pipeline %li.pipelines-tab = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4d957e0d890..faf28db68d1 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -47,13 +47,7 @@ %tbody %th Status %th Commit - - stages.each do |stage| - %th.stage - - if stage.titleize.length > 12 - %span.has-tooltip{ title: "#{stage.titleize}" } - = stage.titleize - - else - = stage.titleize + %th Stages %th %th = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index bdbf3e5f4d6..a5a5619fa12 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -3,11 +3,11 @@ = 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 edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do - Edit - - if can?(current_user, :update_project_snippet, @snippet) = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do Delete + - if can?(current_user, :update_project_snippet, @snippet) + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + Edit - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -21,9 +21,9 @@ New Snippet - if can?(current_user, :update_project_snippet, @snippet) %li - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do - Edit - - if can?(current_user, :update_project_snippet, @snippet) - %li = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do Delete + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do + Edit diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index bae4d8f349f..b70fda88a79 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,15 +1,14 @@ - page_title @snippet.title, "Snippets" -.snippet-holder - = render 'shared/snippets/header' += render 'shared/snippets/header' - %article.file-holder.file-holder-no-border.snippet-file-content - .file-title.file-title-clear - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions.hidden-xs - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' +%article.file-holder.snippet-file-content + .file-title + = blob_icon 0, @snippet.file_name + = @snippet.file_name + .file-actions + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' - %div#notes= render "projects/notes/notes_with_form" +%div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml index 6c43f822db4..07cee86ba4c 100644 --- a/app/views/projects/variables/_table.html.haml +++ b/app/views/projects/variables/_table.html.haml @@ -9,7 +9,7 @@ %th Value %th %tbody - - @project.variables.each do |variable| + - @project.variables.order_key_asc.each do |variable| - if variable.id? %tr %td= variable.key diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index 290743feb4a..6f0a0ea36ec 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,4 +1,4 @@ -- blob = @project.repository.parse_search_result(blob) +- blob = parse_search_result(blob) .blob-result .file-holder .file-title diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index 235106c4f74..648d0bd76cb 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,4 +1,4 @@ -- wiki_blob = @project.repository.parse_search_result(wiki_blob) +- wiki_blob = parse_search_result(wiki_blob) .blob-result .file-holder .file-title diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index 107ad19177c..add4536a0a2 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,7 +1,7 @@ .form-group.project-visibility-level-holder = f.label :visibility_level, class: 'control-label' do Visibility Level - = link_to "(?)", help_page_path("public_access/public_access") + = link_to icon('question-circle'), help_page_path("public_access/public_access") .col-sm-10 - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3856a4917b4..04373684ee9 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -19,7 +19,7 @@ = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector', title: title, filter: true, placeholder: 'Filter', footer_content: true, - data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do + data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do %ul.dropdown-footer-list %li %a.reset-template diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index af753496260..7ae4211ddfd 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -6,12 +6,13 @@ %strong.item-title Snippet #{@snippet.to_reference} %span.creator - created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")} + authored = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - if @snippet.updated_at != @snippet.created_at %span = icon('edit', title: 'edited') = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') + by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} .snippet-actions - if @snippet.project_id? @@ -19,6 +20,5 @@ - else = render "snippets/actions" -.content-block.second-block - %h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author +%h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index c96dfefe17f..ea17bec8677 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -3,19 +3,30 @@ .title = link_to reliable_snippet_path(snippet) do - = truncate(snippet.title, length: 60) + = snippet.title - if snippet.private? - %span.label.label-gray + %span.label.label-gray.hidden-xs = icon('lock') private - %span.monospace.pull-right + %span.monospace.pull-right.hidden-xs = snippet.file_name - %small.pull-right.cgray + %ul.controls.visible-xs + %li + - note_count = snippet.notes.user.count + = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do + = icon('comments') + = note_count + %li + %span.sr-only + = visibility_level_label(snippet.visibility_level) + = visibility_level_icon(snippet.visibility_level, fw: false) + + %small.pull-right.cgray.hidden-xs - if snippet.project_id? = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) - .snippet-info + .snippet-info.hidden-xs = link_to user_snippets_path(snippet.author) do = snippet.author_name authored #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 160c6cd84da..fdaca199218 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -2,12 +2,12 @@ - if current_user = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do New Snippet - - if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do - Edit - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete + - if can?(current_user, :update_personal_snippet, @snippet) + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + Edit - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -18,11 +18,11 @@ %li = link_to new_snippet_path, title: "New Snippet" do New Snippet - - if can?(current_user, :update_personal_snippet, @snippet) - %li - = link_to edit_snippet_path(@snippet) do - Edit - if can?(current_user, :admin_personal_snippet, @snippet) %li = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do Delete + - if can?(current_user, :update_personal_snippet, @snippet) + %li + = link_to edit_snippet_path(@snippet) do + Edit diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index ed3992650d4..fa403da8f79 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,13 +1,12 @@ - page_title @snippet.title, "Snippets" -.snippet-holder - = render 'shared/snippets/header' += render 'shared/snippets/header' - %article.file-holder.file-holder-no-border.snippet-file-content - .file-title.file-title-clear - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions.hidden-xs - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") - = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' +%article.file-holder.snippet-file-content + .file-title + = blob_icon 0, @snippet.file_name + = @snippet.file_name + .file-actions + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 52522e099e7..be22085b0df 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -68,7 +68,8 @@ if Gitlab::Metrics.enabled? ['app', 'mailers', 'emails'] => ['app', 'mailers'], ['app', 'services', '**'] => ['app', 'services'], ['lib', 'gitlab', 'diff'] => ['lib'], - ['lib', 'gitlab', 'email', 'message'] => ['lib'] + ['lib', 'gitlab', 'email', 'message'] => ['lib'], + ['lib', 'gitlab', 'checks'] => ['lib'] } paths_to_instrument.each do |(path, prefix)| diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 49e6e2361b1..650b410595c 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -3,9 +3,13 @@ class Gitlab::Seeder::Pipelines BUILDS = [ { name: 'build:linux', stage: 'build', status: :success }, { name: 'build:osx', stage: 'build', status: :success }, - { name: 'rspec:linux', stage: 'test', status: :success }, - { name: 'rspec:windows', stage: 'test', status: :success }, - { name: 'rspec:windows', stage: 'test', status: :success }, + { name: 'rspec:linux 0 3', stage: 'test', status: :success }, + { name: 'rspec:linux 1 3', stage: 'test', status: :success }, + { name: 'rspec:linux 2 3', stage: 'test', status: :success }, + { name: 'rspec:windows 0 3', stage: 'test', status: :success }, + { name: 'rspec:windows 1 3', stage: 'test', status: :success }, + { name: 'rspec:windows 2 3', stage: 'test', status: :success }, + { name: 'rspec:windows 2 3', stage: 'test', status: :success }, { name: 'rspec:osx', stage: 'test', status_event: :success }, { name: 'spinach:linux', stage: 'test', status: :success }, { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true}, diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb index 84463727b3b..e8de7ccf3db 100644 --- a/db/migrate/20140502125220_migrate_repo_size.rb +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -1,12 +1,15 @@ # rubocop:disable all class MigrateRepoSize < ActiveRecord::Migration + DOWNTIME = false + def up project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id') project_data.each do |project| id = project['id'] namespace_path = project['namespace_path'] || '' - path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git') + repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default + path = File.join(repos_path, namespace_path, project['project_path'] + '.git') begin repo = Gitlab::Git::Repository.new(path) diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb index c8cbd2718ff..75a3eb15124 100644 --- a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb +++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb @@ -8,14 +8,28 @@ class MergeRequestDiffRemoveUniq < ActiveRecord::Migration DOWNTIME = false def up - if index_exists?(:merge_request_diffs, :merge_request_id) - remove_index :merge_request_diffs, :merge_request_id + constraint_name = 'merge_request_diffs_merge_request_id_key' + + transaction do + if index_exists?(:merge_request_diffs, :merge_request_id) + remove_index(:merge_request_diffs, :merge_request_id) + end + + # In some bizarre cases PostgreSQL might have a separate unique constraint + # that we'll need to drop. + if constraint_exists?(constraint_name) && Gitlab::Database.postgresql? + execute("ALTER TABLE merge_request_diffs DROP CONSTRAINT IF EXISTS #{constraint_name};") + end end end def down unless index_exists?(:merge_request_diffs, :merge_request_id) - add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true + add_concurrent_index(:merge_request_diffs, :merge_request_id, unique: true) end end + + def constraint_exists?(name) + indexes(:merge_request_diffs).map(&:name).include?(name) + end end diff --git a/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb new file mode 100644 index 00000000000..fd413d1ca8c --- /dev/null +++ b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.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 AddLfsEnabledToNamespaces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :namespaces, :lfs_enabled, :boolean + end +end diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb new file mode 100644 index 00000000000..18ea9d43a43 --- /dev/null +++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveProjectsPushesSinceGc < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'This migration removes an existing column' + + disable_ddl_transaction! + + def up + remove_column :projects, :pushes_since_gc + end + + def down + add_column_with_default :projects, :pushes_since_gc, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 5c283141084..70279f372c9 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: 20160902122721) do +ActiveRecord::Schema.define(version: 20160913162434) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -650,6 +650,7 @@ ActiveRecord::Schema.define(version: 20160902122721) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: true, null: false t.datetime "deleted_at" + t.boolean "lfs_enabled" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree @@ -824,7 +825,6 @@ ActiveRecord::Schema.define(version: 20160902122721) do t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false t.boolean "public_builds", default: true, null: false - t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" t.boolean "container_registry_enabled" diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md index 2a71b087f19..b6d79706a84 100644 --- a/doc/api/ci/builds.md +++ b/doc/api/ci/builds.md @@ -38,6 +38,15 @@ POST /ci/api/v1/builds/register curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n" ``` +**Responses:** + +| Status | Data |Description | +|--------|------|---------------------------------------------------------------------------| +| `201` | yes | When a build is scheduled for a runner | +| `204` | no | When no builds are scheduled for a runner (for GitLab Runner >= `v1.3.0`) | +| `403` | no | When invalid token is used or no token is sent | +| `404` | no | When no builds are scheduled for a runner (for GitLab Runner < `v1.3.0`) **or** when the runner is set to `paused` in GitLab runner's configuration page | + ### Update details of an existing build ``` diff --git a/doc/api/groups.md b/doc/api/groups.md index a898387eaa2..3e94e1e4efe 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -288,6 +288,7 @@ Parameters: - `path` (required) - The path of the group - `description` (optional) - The group's description - `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. +- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group ## Transfer project to group @@ -317,6 +318,7 @@ PUT /groups/:id | `path` | string | no | The path of the group | | `description` | string | no | The description of the group | | `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | +| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index c835ebc2d44..c40cdd55ea5 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -105,7 +105,8 @@ What is important is that each job is run independently from each other. If you want to check whether your `.gitlab-ci.yml` file is valid, there is a Lint tool under the page `/ci/lint` of your GitLab instance. You can also find -the link under **Settings > CI settings** in your project. +a "CI Lint" button to go to this page under **Pipelines > Pipelines** and +**Pipelines > Builds** in your project. For more information and a complete `.gitlab-ci.yml` syntax, please read [the documentation on .gitlab-ci.yml](../yaml/README.md). diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index 011c2b0e969..8017c36587e 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -70,7 +70,7 @@ sudo -u git -H git checkout 8-12-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.4.0 +sudo -u git -H git checkout v3.5.0 ``` ### 6. Update gitlab-workhorse diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 1498cb361c8..f1b75298180 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -63,7 +63,7 @@ The following table depicts the various user permission levels in a project. | Force push to protected branches [^2] | | | | | | | Remove protected branches [^2] | | | | | | -[^1]: If **Allow guest to access builds** is enabled in CI settings +[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines** [^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner ## Group diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md index f79535d1542..5af9a5d049c 100644 --- a/doc/user/project/merge_requests.md +++ b/doc/user/project/merge_requests.md @@ -93,6 +93,9 @@ A merge request contains all the history from a repository, plus the additional commits added to the branch associated with the merge request. Here's a few tricks to checkout a merge request locally. +Please note that you can checkout a merge request locally even if the source +project is a fork (even a private fork) of the target project. + #### Checkout locally by adding a git alias Add the following alias to your `~/.gitconfig`: diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 306caabf6e6..370d885d366 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -15,6 +15,7 @@ At its current state, GitHub importer can import: - the wiki pages (introduced in GitLab 8.4)
- the milestones (introduced in GitLab 8.7)
- the labels (introduced in GitLab 8.7)
+- the release note descriptions (introduced in GitLab 8.12)
With GitLab 8.7+, references to pull requests and issues are preserved.
diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4f736e4ec2b..bfee4b6c752 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -86,7 +86,8 @@ module API expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) } expose :created_at, :last_activity_at - expose :shared_runners_enabled, :lfs_enabled + expose :shared_runners_enabled + expose :lfs_enabled?, as: :lfs_enabled expose :creator_id expose :namespace expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } @@ -121,6 +122,7 @@ module API class Group < Grape::Entity expose :id, :name, :path, :description, :visibility_level + expose :lfs_enabled?, as: :lfs_enabled expose :avatar_url expose :web_url end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index d2df77238d5..60ac9bdfa33 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -27,13 +27,14 @@ module API # path (required) - The path of the group # description (optional) - The description of the group # visibility_level (optional) - The visibility level of the group + # lfs_enabled (optional) - Enable/disable LFS for the projects in this group # Example Request: # POST /groups post do authorize! :create_group required_attributes! [:name, :path] - attrs = attributes_for_keys [:name, :path, :description, :visibility_level] + attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled] @group = Group.new(attrs) if @group.save @@ -51,13 +52,14 @@ module API # path (optional) - The path of the group # description (optional) - The description of the group # visibility_level (optional) - The visibility level of the group + # lfs_enabled (optional) - Enable/disable LFS for the projects in this group # Example Request: # PUT /groups/:id put ':id' do group = find_group(params[:id]) authorize! :admin_group, group - attrs = attributes_for_keys [:name, :path, :description, :visibility_level] + attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled] if ::Groups::UpdateService.new(group, current_user, attrs).execute present group, with: Entities::GroupDetail diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 6a20ba95a79..150875ed4f0 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -269,6 +269,10 @@ module API render_api_error!('304 Not Modified', 304) end + def no_content! + render_api_error!('204 No Content', 204) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb index 2e2c8da311e..e7a1ec8457d 100644 --- a/lib/banzai/filter/wiki_link_filter/rewriter.rb +++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb @@ -31,6 +31,7 @@ module Banzai def apply_relative_link_rules! if @uri.relative? && @uri.path.present? link = ::File.join(@wiki_base_path, @uri.path) + link = "#{link}##{@uri.fragment}" if @uri.fragment @uri = Addressable::URI.parse(link) end end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 54db63d4628..59f85416ee5 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -27,7 +27,7 @@ module Ci else Gitlab::Metrics.add_event(:build_not_found) - not_found! + build_not_found! end end diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index bcabf7a21b2..ba80c89df78 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -32,6 +32,14 @@ module Ci end end + def build_not_found! + if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) + no_content! + else + not_found! + end + end + def current_runner @runner ||= Runner.find_by_token(params[:token].to_s) end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 839a4fa30d5..c412249a01e 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -195,7 +195,7 @@ module Gitlab # Create (if necessary) and link the secret token file def generate_and_link_secret_token secret_file = Gitlab.config.gitlab_shell.secret_file - unless File.exist? secret_file + unless File.size?(secret_file) # Generate a new token of 16 random hexadecimal characters and store it in secret_file. token = SecureRandom.hex(16) File.write(secret_file, token) diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 4b32eb966aa..cb1065223d4 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -23,6 +23,7 @@ module Gitlab protected def protected_branch_checks + return unless @branch_name return unless project.protected_branch?(@branch_name) if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches) diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index 9b681e636c7..bd90d24a2ec 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -17,11 +17,13 @@ module Gitlab def trigger(gl_id, oldrev, newrev, ref) return [true, nil] unless exists? - case name - when "pre-receive", "post-receive" - call_receive_hook(gl_id, oldrev, newrev, ref) - when "update" - call_update_hook(gl_id, oldrev, newrev, ref) + Bundler.with_clean_env do + case name + when "pre-receive", "post-receive" + call_receive_hook(gl_id, oldrev, newrev, ref) + when "update" + call_update_hook(gl_id, oldrev, newrev, ref) + end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 0388c58f811..d35ee2a1c65 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -24,6 +24,7 @@ module Gitlab import_issues import_pull_requests import_wiki + import_releases handle_errors true @@ -177,6 +178,18 @@ module Gitlab errors << { type: :wiki, errors: e.message } end 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 } + end + end + end end end end diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb new file mode 100644 index 00000000000..73d643b00ad --- /dev/null +++ b/lib/gitlab/github_import/release_formatter.rb @@ -0,0 +1,23 @@ +module Gitlab + module GithubImport + class ReleaseFormatter < BaseFormatter + def attributes + { + project: project, + tag: raw_data.tag_name, + description: raw_data.body, + created_at: raw_data.created_at, + updated_at: raw_data.created_at + } + end + + def klass + Release + end + + def valid? + !raw_data.draft + end + end + end +end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 6d9379acf25..d1e33ea8678 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -22,10 +22,6 @@ module Gitlab private - def repos_path - Gitlab.config.gitlab_shell.repos_path - end - def path_to_repo @project.repository.path_to_repo end diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index a0fd41161a5..cc74bb29087 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -18,18 +18,18 @@ module Gitlab FileUtils.mkdir_p(path) end - @cmd_output = "" - @cmd_status = 0 + cmd_output = "" + cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| yield(stdin) if block_given? stdin.close - @cmd_output << stdout.read - @cmd_output << stderr.read - @cmd_status = wait_thr.value.exitstatus + cmd_output << stdout.read + cmd_output << stderr.read + cmd_status = wait_thr.value.exitstatus end - [@cmd_output, @cmd_status] + [cmd_output, cmd_status] end end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index efe4aeb399d..60aae541d46 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -102,7 +102,7 @@ module Gitlab def secret @secret ||= begin - bytes = Base64.strict_decode64(File.read(secret_path)) + bytes = Base64.strict_decode64(File.read(secret_path).chomp) raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH bytes end diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake new file mode 100644 index 00000000000..609dfaa48e3 --- /dev/null +++ b/lib/tasks/haml-lint.rake @@ -0,0 +1,5 @@ +unless Rails.env.production? + require 'haml_lint/rake_task' + + HamlLint::RakeTask.new +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index ffe0641ddd7..b0f740f48f7 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -181,6 +181,25 @@ describe ProjectsController do expect(response).to have_http_status(302) expect(response).to redirect_to(dashboard_projects_path) end + + context "when the project is forked" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "closes all related merge requests" do + project.merge_requests << merge_request + sign_in(admin) + + delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path + + expect(merge_request.reload.state).to eq('closed') + end + end end describe "POST #toggle_star" do diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index 5b645fab32e..45eaebb2576 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -30,5 +30,9 @@ FactoryGirl.define do trait :shared do is_shared true end + + trait :inactive do + active false + end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index c6c2e2095df..e51586d32ec 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe 'Issue Boards', feature: true, js: true do include WaitForAjax + include WaitForVueResource let(:project) { create(:empty_project, :public) } let(:user) { create(:user) } @@ -187,13 +188,13 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Showing 20 of 56 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - wait_for_vue_resource(spinner: false) + wait_for_vue_resource expect(page).to have_selector('.card', count: 40) expect(page).to have_content('Showing 40 of 56 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - wait_for_vue_resource(spinner: false) + wait_for_vue_resource expect(page).to have_selector('.card', count: 56) expect(page).to have_content('Showing all issues') @@ -372,7 +373,7 @@ describe 'Issue Boards', feature: true, js: true do page.within '.dropdown-menu-author' do click_link(user2.name) end - wait_for_vue_resource(spinner: false) + wait_for_vue_resource expect(find('.js-author-search')).to have_content(user2.name) end @@ -398,7 +399,7 @@ describe 'Issue Boards', feature: true, js: true do page.within '.dropdown-menu-assignee' do click_link(user.name) end - wait_for_vue_resource(spinner: false) + wait_for_vue_resource expect(find('.js-assignee-search')).to have_content(user.name) end @@ -424,7 +425,7 @@ describe 'Issue Boards', feature: true, js: true do page.within '.milestone-filter' do click_link(milestone.title) end - wait_for_vue_resource(spinner: false) + wait_for_vue_resource expect(find('.js-milestone-select')).to have_content(milestone.title) end @@ -449,7 +450,7 @@ describe 'Issue Boards', feature: true, js: true do page.within '.dropdown-menu-labels' do click_link(testing.title) - wait_for_vue_resource(spinner: false) + wait_for_vue_resource find('.dropdown-menu-close').click end end @@ -478,7 +479,7 @@ describe 'Issue Boards', feature: true, js: true do page.within '.dropdown-menu-labels' do click_link(testing.title) - wait_for_vue_resource(spinner: false) + wait_for_vue_resource find('.dropdown-menu-close').click end end @@ -509,9 +510,9 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.dropdown-menu-labels')) do click_link(testing.title) - wait_for_vue_resource(spinner: false) + wait_for_vue_resource click_link(bug.title) - wait_for_vue_resource(spinner: false) + wait_for_vue_resource find('.dropdown-menu-close').click end end @@ -536,7 +537,7 @@ describe 'Issue Boards', feature: true, js: true do page.within '.dropdown-menu-labels' do click_link("No Label") - wait_for_vue_resource(spinner: false) + wait_for_vue_resource find('.dropdown-menu-close').click end end @@ -559,7 +560,7 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_selector('.card', count: 6) expect(find('.card', match: :first)).to have_content(bug.title) click_button(bug.title) - wait_for_vue_resource(spinner: false) + wait_for_vue_resource end wait_for_vue_resource @@ -584,7 +585,7 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.card', match: :first)) do click_button(bug.title) end - wait_for_vue_resource(spinner: false) + wait_for_vue_resource expect(page).to have_selector('.card', count: 1) end @@ -647,14 +648,4 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end - - def wait_for_vue_resource(spinner: true) - Timeout.timeout(Capybara.default_max_wait_time) do - loop until page.evaluate_script('Vue.activeResources').zero? - end - - if spinner - expect(find('.boards-list')).not_to have_selector('.fa-spinner') - end - end end diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb new file mode 100644 index 00000000000..7ef68e9eb8d --- /dev/null +++ b/spec/features/boards/keyboard_shortcut_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe 'Issue Boards shortcut', feature: true, js: true do + include WaitForVueResource + + let(:project) { create(:empty_project) } + + before do + project.create_board + project.board.lists.create(list_type: :backlog) + project.board.lists.create(list_type: :done) + + login_as :admin + + visit namespace_project_path(project.namespace, project) + end + + it 'takes user to issue board index' do + find('body').native.send_keys('gl') + expect(page).to have_selector('.boards-list') + + wait_for_vue_resource + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d744d0eb6af..22359c8f938 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -144,7 +144,7 @@ describe 'Issues', feature: true do visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) expect(page).to have_content 'foobar' - expect(page.all('.issue-no-comments').first.text).to eq "0" + expect(page.all('.no-comments').first.text).to eq "0" end end diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb new file mode 100644 index 00000000000..3b20d38c520 --- /dev/null +++ b/spec/features/profiles/keys_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +describe '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 + fill_in('Key', with: attributes_for(:key).fetch(:key)) + + expect(find_field('Title').value).to eq 'dummy@gitlab.com' + end + end +end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 4a83740621a..f76c4fe8b57 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -13,10 +13,12 @@ feature 'issuable templates', feature: true, js: true do context 'user creates an issue using templates' do let(:template_content) { 'this is a test "bug" template' } + let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) } 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) + project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false) visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -27,6 +29,17 @@ feature 'issuable templates', feature: true, js: true do preview_template save_changes end + + it 'updates height of markdown textarea' do + start_height = page.evaluate_script('$(".markdown-area").outerHeight()') + + select_template 'test' + wait_for_ajax + + end_height = page.evaluate_script('$(".markdown-area").outerHeight()') + + expect(end_height).not_to eq(start_height) + end end context 'user creates a merge request using templates' do @@ -51,7 +64,7 @@ feature 'issuable templates', feature: true, js: true do let(:template_content) { 'this is a test "feature-proposal" template' } let(:fork_user) { create(:user) } let(:fork_project) { create(:project, :public) } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) } background do logout @@ -59,16 +72,20 @@ feature 'issuable templates', feature: true, js: true do fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) login_as fork_user - fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) - visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request + project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end - scenario 'user selects "feature-proposal" template' do - select_template 'feature-proposal' - wait_for_ajax - preview_template - save_changes + context 'feature proposal template' do + context 'template exists in target project' do + scenario 'user selects template' do + select_template 'feature-proposal' + wait_for_ajax + preview_template + save_changes + end + end end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index e00d85904d5..2242cb6236a 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -57,7 +57,7 @@ feature 'Project', feature: true do describe 'removal', js: true do let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, namespace: user.namespace, name: 'project1') } before do login_with(user) @@ -65,8 +65,12 @@ feature 'Project', feature: true do visit edit_namespace_project_path(project.namespace, project) end - it 'removes project' do + it 'removes a project' do expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) + expect(page).to have_content "Project 'project1' will be deleted." + expect(Project.all.count).to be_zero + expect(project.issues).to be_empty + expect(project.merge_requests).to be_empty end end diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb index 83cf306437d..b9e66243d84 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/todos/todos_filtering_spec.rb @@ -29,8 +29,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do fill_in 'Search projects', with: project_1.name_with_namespace click_link project_1.name_with_namespace end + wait_for_ajax - expect('.prepend-top-default').not_to have_content project_2.name_with_namespace + + expect(page).to have_content project_1.name_with_namespace + expect(page).not_to have_content project_2.name_with_namespace end it 'filters by author' do @@ -39,8 +42,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do fill_in 'Search authors', with: user_1.name click_link user_1.name end + wait_for_ajax - expect('.prepend-top-default').not_to have_content user_2.name + + expect(find('.todos-list')).to have_content user_1.name + expect(find('.todos-list')).not_to have_content user_2.name end it 'filters by type' do @@ -48,8 +54,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do within '.dropdown-menu-type' do click_link 'Issue' end + wait_for_ajax - expect('.prepend-top-default').not_to have_content ' merge request !' + + expect(find('.todos-list')).to have_content issue.to_reference + expect(find('.todos-list')).not_to have_content merge_request.to_reference end it 'filters by action' do @@ -57,7 +66,10 @@ describe 'Dashboard > User filters todos', feature: true, js: true do within '.dropdown-menu-action' do click_link 'Assigned' end + wait_for_ajax - expect('.prepend-top-default').not_to have_content ' mentioned ' + + expect(find('.todos-list')).to have_content ' assigned you ' + expect(find('.todos-list')).not_to have_content ' mentioned ' end end diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb index 356a8d668b0..f00abd82fea 100644 --- a/spec/features/users/snippets_spec.rb +++ b/spec/features/users/snippets_spec.rb @@ -19,7 +19,10 @@ describe 'Snippets tab on a user profile', feature: true, js: true do end context 'clicking on the link to the second page' do - before { click_link('2') } + 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) diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 0807534720a..233d00534e5 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -18,4 +18,67 @@ describe GroupsHelper do expect(group_icon(group.path)).to match('group_avatar.png') end end + + describe 'group_lfs_status' do + let(:group) { create(:group) } + let!(:project) { create(:empty_project, namespace_id: group.id) } + + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end + + context 'only one project in group' do + before do + group.update_attribute(:lfs_enabled, true) + end + + it 'returns all projects as enabled' do + expect(group_lfs_status(group)).to include('Enabled for all projects') + end + + it 'returns all projects as disabled' do + project.update_attribute(:lfs_enabled, false) + + expect(group_lfs_status(group)).to include('Enabled for 0 out of 1 project') + end + end + + context 'more than one project in group' do + before do + create(:empty_project, namespace_id: group.id) + end + + context 'LFS enabled in group' do + before do + group.update_attribute(:lfs_enabled, true) + end + + it 'returns both projects as enabled' do + expect(group_lfs_status(group)).to include('Enabled for all projects') + end + + it 'returns only one as enabled' do + project.update_attribute(:lfs_enabled, false) + + expect(group_lfs_status(group)).to include('Enabled for 1 out of 2 projects') + end + end + + context 'LFS disabled in group' do + before do + group.update_attribute(:lfs_enabled, false) + end + + it 'returns both projects as disabled' do + expect(group_lfs_status(group)).to include('Disabled for all projects') + end + + it 'returns only one as disabled' do + project.update_attribute(:lfs_enabled, true) + + expect(group_lfs_status(group)).to include('Disabled for 1 out of 2 projects') + end + end + end + end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 4b2ca3514f8..c5b5aa8c445 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -6,6 +6,38 @@ describe SearchHelper do str end + describe 'parsing result' do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:results) { repository.search_files('feature', 'master') } + let(:search_result) { results.first } + + subject { helper.parse_search_result(search_result) } + + it "returns a valid OpenStruct object" do + is_expected.to be_an OpenStruct + expect(subject.filename).to eq('CHANGELOG') + expect(subject.basename).to eq('CHANGELOG') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(186) + expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") + end + + context "when filename has extension" do + let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + + it { expect(subject.filename).to eq('CONTRIBUTE.md') } + it { expect(subject.basename).to eq('CONTRIBUTE') } + end + + context "when file under directory" do + let(:search_result) { "master:a/b/c.md:5:a b c\n" } + + it { expect(subject.filename).to eq('a/b/c.md') } + it { expect(subject.basename).to eq('a/b/c') } + end + end + describe 'search_autocomplete_source' do context "with no current user" do before do diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 51c89ac4889..ac9bde6baf1 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -127,6 +127,13 @@ describe Banzai::Pipeline::WikiPipeline do expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") end + + it 'rewrites links with anchor' do + markdown = '[Link to Header](start-page#title)' + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"") + end end describe "when creating root links" do diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 7df288f619f..553c849c9b4 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -98,6 +98,30 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + let(:release1) do + double( + tag_name: 'v1.0.0', + name: 'First release', + body: 'Release v1.0.0', + draft: false, + created_at: created_at, + updated_at: updated_at, + url: 'https://api.github.com/repos/octocat/Hello-World/releases/1' + ) + end + + let(:release2) do + double( + tag_name: 'v2.0.0', + name: 'Second release', + body: nil, + draft: false, + created_at: created_at, + updated_at: updated_at, + url: 'https://api.github.com/repos/octocat/Hello-World/releases/2' + ) + end + before do allow(project).to receive(:import_data).and_return(double.as_null_object) allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) @@ -106,6 +130,7 @@ describe Gitlab::GithubImport::Importer, lib: true do 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(:last_response).and_return(double(rels: { next: nil })) + allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) end @@ -127,8 +152,9 @@ describe Gitlab::GithubImport::Importer, lib: true do { 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: :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" } + ] } described_class.new(project).execute diff --git a/spec/lib/gitlab/github_import/release_formatter_spec.rb b/spec/lib/gitlab/github_import/release_formatter_spec.rb new file mode 100644 index 00000000000..793128c6ab9 --- /dev/null +++ b/spec/lib/gitlab/github_import/release_formatter_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::ReleaseFormatter, lib: true do + let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } + let(:octocat) { double(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + + let(:base_data) do + { + tag_name: 'v1.0.0', + name: 'First release', + draft: false, + created_at: created_at, + published_at: created_at, + body: 'Release v1.0.0' + } + end + + subject(:release) { described_class.new(project, raw_data) } + + describe '#attributes' do + let(:raw_data) { double(base_data) } + + it 'returns formatted attributes' do + expected = { + project: project, + tag: 'v1.0.0', + description: 'Release v1.0.0', + created_at: created_at, + updated_at: created_at + } + + expect(release.attributes).to eq(expected) + end + end + + describe '#valid' do + context 'when release is not a draft' do + let(:raw_data) { double(base_data) } + + it 'returns true' do + expect(release.valid?).to eq true + end + end + + context 'when release is draft' do + let(:raw_data) { double(base_data.merge(draft: true)) } + + it 'returns false' do + expect(release.valid?).to eq false + end + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 395192149a9..6c7fa7e7c15 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -30,6 +30,11 @@ describe Gitlab::Workhorse, lib: true do expect(subject.encoding).to eq(Encoding::ASCII_8BIT) end + it 'accepts a trailing newline' do + open(described_class.secret_path, 'a') { |f| f.write "\n" } + expect(subject.length).to eq(32) + end + it 'raises an exception if the secret file cannot be read' do File.delete(described_class.secret_path) expect { subject }.to raise_exception(Errno::ENOENT) diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index cee20234e1f..03d02b4d382 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require 'rails_helper' describe Blob do @@ -7,6 +8,25 @@ describe Blob do end end + describe '#data' do + context 'using a binary blob' do + it 'returns the data as-is' do + data = "\n\xFF\xB9\xC3" + blob = described_class.new(double(binary?: true, data: data)) + + expect(blob.data).to eq(data) + end + end + + context 'using a text blob' do + it 'converts the data to UTF-8' do + blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3")) + + expect(blob.data).to eq("\n���") + end + end + end + describe '#svg?' do it 'is falsey when not text' do git_blob = double(text?: false) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index fbf945c757c..f1857f846dc 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -373,8 +373,8 @@ describe Ci::Pipeline, models: true do end describe '#execute_hooks' do - let!(:build_a) { create_build('a') } - let!(:build_b) { create_build('b') } + let!(:build_a) { create_build('a', 0) } + let!(:build_b) { create_build('b', 1) } let!(:hook) do create(:project_hook, project: project, pipeline_events: enabled) @@ -398,7 +398,7 @@ describe Ci::Pipeline, models: true do build_b.enqueue end - it 'receive a pending event once' do + it 'receives a pending event once' do expect(WebMock).to have_requested_pipeline_hook('pending').once end end @@ -411,7 +411,7 @@ describe Ci::Pipeline, models: true do build_b.run end - it 'receive a running event once' do + it 'receives a running event once' do expect(WebMock).to have_requested_pipeline_hook('running').once end end @@ -422,11 +422,21 @@ describe Ci::Pipeline, models: true do build_b.success end - it 'receive a success event once' do + it 'receives a success event once' do expect(WebMock).to have_requested_pipeline_hook('success').once end end + context 'when stage one failed' do + before do + build_a.drop + end + + it 'receives a failed event once' do + expect(WebMock).to have_requested_pipeline_hook('failed').once + end + end + def have_requested_pipeline_hook(status) have_requested(:post, hook.url).with do |req| json_body = JSON.parse(req.body) @@ -450,8 +460,12 @@ describe Ci::Pipeline, models: true do end end - def create_build(name) - create(:ci_build, :created, pipeline: pipeline, name: name) + def create_build(name, stage_idx) + create(:ci_build, + :created, + pipeline: pipeline, + name: name, + stage_idx: stage_idx) end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index fcfa3138ce5..2f1baff5d66 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -40,7 +40,7 @@ describe CommitStatus, models: true do it { is_expected.to be_falsey } end - %w(running success failed).each do |status| + %w[running success failed].each do |status| context "if commit status is #{status}" do before { commit_status.status = status } @@ -48,7 +48,7 @@ describe CommitStatus, models: true do end end - %w(pending canceled).each do |status| + %w[pending canceled].each do |status| context "if commit status is #{status}" do before { commit_status.status = status } @@ -60,7 +60,7 @@ describe CommitStatus, models: true do describe '#active?' do subject { commit_status.active? } - %w(pending running).each do |state| + %w[pending running].each do |state| context "if commit_status.status is #{state}" do before { commit_status.status = state } @@ -68,7 +68,7 @@ describe CommitStatus, models: true do end end - %w(success failed canceled).each do |state| + %w[success failed canceled].each do |state| context "if commit_status.status is #{state}" do before { commit_status.status = state } @@ -80,7 +80,7 @@ describe CommitStatus, models: true do describe '#complete?' do subject { commit_status.complete? } - %w(success failed canceled).each do |state| + %w[success failed canceled].each do |state| context "if commit_status.status is #{state}" do before { commit_status.status = state } @@ -88,7 +88,7 @@ describe CommitStatus, models: true do end end - %w(pending running).each do |state| + %w[pending running].each do |state| context "if commit_status.status is #{state}" do before { commit_status.status = state } @@ -187,7 +187,7 @@ describe CommitStatus, models: true do subject { CommitStatus.where(pipeline: pipeline).stages } it 'returns ordered list of stages' do - is_expected.to eq(%w(build test deploy)) + is_expected.to eq(%w[build test deploy]) end end @@ -223,4 +223,33 @@ describe CommitStatus, models: true do expect(commit_status.commit).to eq project.commit end end + + describe '#group_name' do + subject { commit_status.group_name } + + tests = { + 'rspec:windows' => 'rspec:windows', + 'rspec:windows 0' => 'rspec:windows 0', + 'rspec:windows 0 test' => 'rspec:windows 0 test', + 'rspec:windows 0 1' => 'rspec:windows', + 'rspec:windows 0 1 name' => 'rspec:windows name', + 'rspec:windows 0/1' => 'rspec:windows', + 'rspec:windows 0/1 name' => 'rspec:windows name', + 'rspec:windows 0:1' => 'rspec:windows', + 'rspec:windows 0:1 name' => 'rspec:windows name', + 'rspec:windows 10000 20000' => 'rspec:windows', + 'rspec:windows 0 : / 1' => 'rspec:windows', + 'rspec:windows 0 : / 1 name' => 'rspec:windows name', + '0 1 name ruby' => 'name ruby', + '0 :/ 1 name ruby' => 'name ruby' + } + + tests.each do |name, group_name| + it "'#{name}' puts in '#{group_name}'" do + commit_status.name = name + + is_expected.to eq(group_name) + end + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ea4b59c26b1..0b3ef9b98fd 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -187,6 +187,52 @@ describe Group, models: true do it { expect(group.has_master?(@members[:requester])).to be_falsey } end + describe '#lfs_enabled?' do + context 'LFS enabled globally' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end + + it 'returns true when nothing is set' do + expect(group.lfs_enabled?).to be_truthy + end + + it 'returns false when set to false' do + group.update_attribute(:lfs_enabled, false) + + expect(group.lfs_enabled?).to be_falsey + end + + it 'returns true when set to true' do + group.update_attribute(:lfs_enabled, true) + + expect(group.lfs_enabled?).to be_truthy + end + end + + context 'LFS disabled globally' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) + end + + it 'returns false when nothing is set' do + expect(group.lfs_enabled?).to be_falsey + end + + it 'returns false when set to false' do + group.update_attribute(:lfs_enabled, false) + + expect(group.lfs_enabled?).to be_falsey + end + + it 'returns false when set to true' do + group.update_attribute(:lfs_enabled, true) + + expect(group.lfs_enabled?).to be_falsey + end + end + end + describe '#owners' do let(:owner) { create(:user) } let(:developer) { create(:user) } diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index fef90d9b5cb..0b1634f654a 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(:project) + project = create(:empty_project) group = create(:group) @owner_user = create(:user).tap { |u| group.add_owner(u) } @owner = group.members.find_by(user_id: @owner_user.id) @@ -65,6 +65,15 @@ describe Member, models: true do @master_user = create(:user).tap { |u| project.team << [u, :master] } @master = project.members.find_by(user_id: @master_user.id) + @blocked_user = create(:user).tap do |u| + project.team << [u, :master] + project.team << [u, :developer] + + u.block! + end + @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', @@ -73,7 +82,7 @@ describe Member, models: true do ) @invited_member = project.members.invite.find_by_invite_email('toto1@example.com') - accepted_invite_user = build(:user) + accepted_invite_user = build(:user, state: :active) Member.add_user( project.members, 'toto2@example.com', @@ -91,7 +100,7 @@ describe Member, models: true do describe '.access_for_user_ids' do it 'returns the right access levels' do - users = [@owner_user.id, @master_user.id] + users = [@owner_user.id, @master_user.id, @blocked_user.id] expected = { @owner_user.id => Gitlab::Access::OWNER, @master_user.id => Gitlab::Access::MASTER @@ -125,6 +134,19 @@ describe Member, models: true do it { expect(described_class.request).not_to include @accepted_request_member } end + describe '.developers' do + subject { described_class.developers.to_a } + + it { is_expected.not_to include @owner } + it { is_expected.not_to include @master } + it { is_expected.to include @invited_member } + it { is_expected.to include @accepted_invite_member } + it { is_expected.not_to include @requested_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.not_to include @blocked_master } + it { is_expected.not_to include @blocked_developer } + end + describe '.owners_and_masters' do it { expect(described_class.owners_and_masters).to include @owner } it { expect(described_class.owners_and_masters).to include @master } @@ -132,6 +154,20 @@ describe Member, models: true do it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member } it { expect(described_class.owners_and_masters).not_to include @requested_member } it { expect(described_class.owners_and_masters).not_to include @accepted_request_member } + it { expect(described_class.owners_and_masters).not_to include @blocked_master } + end + + describe '.has_access' do + subject { described_class.has_access.to_a } + + it { is_expected.to include @owner } + it { is_expected.to include @master } + it { is_expected.to include @invited_member } + it { is_expected.to include @accepted_invite_member } + it { is_expected.not_to include @requested_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.not_to include @blocked_master } + it { is_expected.not_to include @blocked_developer } end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 5bf3b8e609e..3b815ded2d3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1038,4 +1038,81 @@ describe MergeRequest, models: true do end end end + + describe '#closed_without_source_project?' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } + let(:destroy_service) { Projects::DestroyService.new(fork_project, user) } + + context 'when the merge request is closed' do + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project) + end + + it 'returns false if the source project exists' do + expect(closed_merge_request.closed_without_source_project?).to be_falsey + end + + it 'returns true if the source project does not exist' do + destroy_service.execute + closed_merge_request.reload + + expect(closed_merge_request.closed_without_source_project?).to be_truthy + end + end + + context 'when the merge request is open' do + it 'returns false' do + expect(subject.closed_without_source_project?).to be_falsey + end + end + end + + describe '#reopenable?' do + context 'when the merge request is closed' do + it 'returns true' do + subject.close + + expect(subject.reopenable?).to be_truthy + end + + context 'forked project' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } + let(:merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project) + end + + it 'returns false if unforked' do + Projects::UnlinkForkService.new(fork_project, user).execute + + expect(merge_request.reload.reopenable?).to be_falsey + end + + it 'returns false if the source project is deleted' do + Projects::DestroyService.new(fork_project, user).execute + + expect(merge_request.reload.reopenable?).to be_falsey + end + + it 'returns false if the merge request is merged' do + merge_request.update_attributes(state: 'merged') + + expect(merge_request.reload.reopenable?).to be_falsey + end + end + end + + context 'when the merge request is opened' do + it 'returns false' do + expect(subject.reopenable?).to be_falsey + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 4a41fafb84d..7ca1bd1e5c9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6,6 +6,7 @@ describe Project, models: true do it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:creator).class_name('User') } it { is_expected.to have_many(:users) } + it { is_expected.to have_many(:services) } it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } it { is_expected.to have_many(:issues).dependent(:destroy) } @@ -24,6 +25,30 @@ describe Project, models: true do it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_one(:board).dependent(:destroy) } + it { is_expected.to have_one(:campfire_service).dependent(:destroy) } + it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) } + it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) } + it { is_expected.to have_one(:builds_email_service).dependent(:destroy) } + it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) } + it { is_expected.to have_one(:irker_service).dependent(:destroy) } + it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) } + it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } + it { is_expected.to have_one(:flowdock_service).dependent(:destroy) } + it { is_expected.to have_one(:assembla_service).dependent(:destroy) } + it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) } + it { is_expected.to have_one(:buildkite_service).dependent(:destroy) } + it { is_expected.to have_one(:bamboo_service).dependent(:destroy) } + it { is_expected.to have_one(:teamcity_service).dependent(:destroy) } + it { is_expected.to have_one(:jira_service).dependent(:destroy) } + it { is_expected.to have_one(:redmine_service).dependent(:destroy) } + it { is_expected.to have_one(:custom_issue_tracker_service).dependent(:destroy) } + it { is_expected.to have_one(:bugzilla_service).dependent(:destroy) } + it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) } + it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) } + it { is_expected.to have_one(:project_feature).dependent(:destroy) } + it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) } + it { is_expected.to have_one(:last_event).class_name('Event') } + it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) } it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:builds) } @@ -31,9 +56,16 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:labels).dependent(:destroy) } + it { is_expected.to have_many(:users_star_projects).dependent(:destroy) } it { is_expected.to have_many(:environments).dependent(:destroy) } it { is_expected.to have_many(:deployments).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } + it { is_expected.to have_many(:releases).dependent(:destroy) } + it { is_expected.to have_many(:lfs_objects_projects).dependent(:destroy) } + it { is_expected.to have_many(:project_group_links).dependent(:destroy) } + it { is_expected.to have_many(:notification_settings).dependent(:destroy) } + it { is_expected.to have_many(:forks).through(:forked_project_links) } describe '#members & #requesters' do let(:project) { create(:project) } @@ -178,7 +210,7 @@ describe Project, models: true do expect(project.runners_token).not_to eq('') end - it 'does not set an random toke if one provided' do + it 'does not set an random token if one provided' do project = FactoryGirl.create :empty_project, runners_token: 'my-token' expect(project.runners_token).to eq('my-token') end @@ -1385,6 +1417,68 @@ describe Project, models: true do end end + describe '#lfs_enabled?' do + let(:project) { create(:project) } + + shared_examples 'project overrides group' do + it 'returns true when enabled in project' do + project.update_attribute(:lfs_enabled, true) + + expect(project.lfs_enabled?).to be_truthy + end + + it 'returns false when disabled in project' do + project.update_attribute(:lfs_enabled, false) + + expect(project.lfs_enabled?).to be_falsey + end + + it 'returns the value from the namespace, when no value is set in project' do + expect(project.lfs_enabled?).to eq(project.namespace.lfs_enabled?) + end + end + + context 'LFS disabled in group' do + before do + project.namespace.update_attribute(:lfs_enabled, false) + enable_lfs + end + + it_behaves_like 'project overrides group' + end + + context 'LFS enabled in group' do + before do + project.namespace.update_attribute(:lfs_enabled, true) + enable_lfs + end + + it_behaves_like 'project overrides group' + end + + describe 'LFS disabled globally' do + shared_examples 'it always returns false' do + it do + expect(project.lfs_enabled?).to be_falsey + expect(project.namespace.lfs_enabled?).to be_falsey + end + end + + context 'when no values are set' do + it_behaves_like 'it always returns false' + end + + context 'when all values are set to true' do + before do + project.namespace.update_attribute(:lfs_enabled, true) + project.update_attribute(:lfs_enabled, true) + end + + it_behaves_like 'it always returns false' + end + end + end + describe '.where_paths_in' do context 'without any paths' do it 'returns an empty relation' do @@ -1497,4 +1591,60 @@ describe Project, models: true do project.change_head(project.default_branch) end end + + describe '#pushes_since_gc' do + let(:project) { create(:project) } + + after do + project.reset_pushes_since_gc + end + + context 'without any pushes' do + it 'returns 0' do + expect(project.pushes_since_gc).to eq(0) + end + end + + context 'with a number of pushes' do + it 'returns the number of pushes' do + 3.times { project.increment_pushes_since_gc } + + expect(project.pushes_since_gc).to eq(3) + end + end + end + + describe '#increment_pushes_since_gc' do + let(:project) { create(:project) } + + after do + project.reset_pushes_since_gc + end + + it 'increments the number of pushes since the last GC' do + 3.times { project.increment_pushes_since_gc } + + expect(project.pushes_since_gc).to eq(3) + end + end + + describe '#reset_pushes_since_gc' do + let(:project) { create(:project) } + + after do + project.reset_pushes_since_gc + end + + it 'resets the number of pushes since the last GC' do + 3.times { project.increment_pushes_since_gc } + + project.reset_pushes_since_gc + + expect(project.pushes_since_gc).to eq(0) + end + end + + def enable_lfs + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7624050878e..94681004c96 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -186,32 +186,6 @@ describe Repository, models: true do it { is_expected.to be_an String } it { expect(subject.lines[2]).to eq("master:CHANGELOG:188: - Feature: Replace teams with group membership\n") } end - - describe 'parsing result' do - subject { repository.parse_search_result(search_result) } - let(:search_result) { results.first } - - it { is_expected.to be_an OpenStruct } - it { expect(subject.filename).to eq('CHANGELOG') } - it { expect(subject.basename).to eq('CHANGELOG') } - it { expect(subject.ref).to eq('master') } - it { expect(subject.startline).to eq(186) } - it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } - - context "when filename has extension" do - let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } - - it { expect(subject.filename).to eq('CONTRIBUTE.md') } - it { expect(subject.basename).to eq('CONTRIBUTE') } - end - - context "when file under directory" do - let(:search_result) { "master:a/b/c.md:5:a b c\n" } - - it { expect(subject.filename).to eq('a/b/c.md') } - it { expect(subject.basename).to eq('a/b/c') } - end - end end describe "#changelog" do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 9e390bea50b..780bd7f2859 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -15,6 +15,25 @@ describe Ci::API::API do describe "POST /builds/register" do let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' } + + shared_examples 'no builds available' do + context 'when runner sends version in User-Agent' do + context 'for stable version' do + it { expect(response).to have_http_status(204) } + end + + context 'for beta version' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (1-5-stable; go1.6.3; linux/amd64)' } + it { expect(response).to have_http_status(204) } + end + end + + context "when runner doesn't send version in User-Agent" do + let(:user_agent) { 'Go-http-client/1.1' } + it { expect(response).to have_http_status(404) } + end + end it "starts a build" do register_builds info: { platform: :darwin } @@ -33,36 +52,30 @@ describe Ci::API::API do context 'when builds are finished' do before do build.success - end - - it "returns 404 error if no builds for specific runner" do register_builds - - expect(response).to have_http_status(404) end + + it_behaves_like 'no builds available' end context 'for other project with builds' do before do build.success create(:ci_build, :pending) - end - - it "returns 404 error if no builds for shared runner" do register_builds - - expect(response).to have_http_status(404) end + + it_behaves_like 'no builds available' end context 'for shared runner' do let(:shared_runner) { create(:ci_runner, token: "SharedRunner") } - it "should return 404 error if no builds for shared runner" do + before do register_builds shared_runner.token - - expect(response).to have_http_status(404) end + + it_behaves_like 'no builds available' end context 'for triggered build' do @@ -136,18 +149,27 @@ describe Ci::API::API do end context 'when runner is not allowed to pick untagged builds' do - before { runner.update_column(:run_untagged, false) } - - it 'does not pick build' do + before do + runner.update_column(:run_untagged, false) register_builds - - expect(response).to have_http_status 404 end + + it_behaves_like 'no builds available' end end + context 'when runner is paused' do + let(:inactive_runner) { create(:ci_runner, :inactive, token: "InactiveRunner") } + + before do + register_builds inactive_runner.token + end + + it { expect(response).to have_http_status 404 } + end + def register_builds(token = runner.token, **params) - post ci_api("/builds/register"), params.merge(token: token) + post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent } end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index ad0d58672b3..c6160f4fa57 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -4,12 +4,11 @@ describe Projects::HousekeepingService do subject { Projects::HousekeepingService.new(project) } let(:project) { create :project } - describe 'execute' do - before do - project.pushes_since_gc = 3 - project.save! - end + after do + project.reset_pushes_since_gc + end + describe '#execute' do it 'enqueues a sidekiq job' do expect(subject).to receive(:try_obtain_lease).and_return(true) expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id) @@ -32,12 +31,12 @@ describe Projects::HousekeepingService do it 'does not reset pushes_since_gc' do expect do expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) - end.not_to change { project.pushes_since_gc }.from(3) + end.not_to change { project.pushes_since_gc } end end end - describe 'needed?' do + describe '#needed?' do it 'when the count is low enough' do expect(subject.needed?).to eq(false) end @@ -48,25 +47,11 @@ describe Projects::HousekeepingService do end end - describe 'increment!' do - let(:lease_key) { "project_housekeeping:increment!:#{project.id}" } - + describe '#increment!' do it 'increments the pushes_since_gc counter' do - lease = double(:lease, try_obtain: true) - expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease) - expect do subject.increment! end.to change { project.pushes_since_gc }.from(0).to(1) end - - it 'does not increment when no lease can be obtained' do - lease = double(:lease, try_obtain: false) - expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease) - - expect do - subject.increment! - end.not_to change { project.pushes_since_gc } - end end end diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb new file mode 100644 index 00000000000..1029f84716f --- /dev/null +++ b/spec/support/wait_for_vue_resource.rb @@ -0,0 +1,7 @@ +module WaitForVueResource + def wait_for_vue_resource(spinner: true) + Timeout.timeout(Capybara.default_max_wait_time) do + loop until page.evaluate_script('Vue.activeResources').zero? + end + end +end diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb new file mode 100644 index 00000000000..c5b16c1c304 --- /dev/null +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe 'projects/pipelines/show' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } + + before do + controller.prepend_view_path('app/views/projects') + + create_build('build', 0, 'build') + create_build('test', 1, 'rspec 0:2') + create_build('test', 1, 'rspec 1:2') + create_build('test', 1, 'audit') + create_build('deploy', 2, 'production') + + create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) + + assign(:project, project) + assign(:pipeline, pipeline) + + allow(view).to receive(:can?).and_return(true) + end + + it 'shows a graph with grouped stages' do + render + + expect(rendered).to have_css('.pipeline-graph') + expect(rendered).to have_css('.grouped-pipeline-dropdown') + + # stages + expect(rendered).to have_text('Build') + expect(rendered).to have_text('Test') + expect(rendered).to have_text('Deploy') + expect(rendered).to have_text('External') + + # builds + expect(rendered).to have_text('rspec') + expect(rendered).to have_text('rspec 0:2') + expect(rendered).to have_text('production') + expect(rendered).to have_text('jenkins') + end + + private + + def create_build(stage, stage_idx, name) + create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name) + end +end |